<?php

namespace App;

use App\Traits\AppliesQueryConditions;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\Request;
use Illuminate\Mail\Attachment;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Maatwebsite\Excel\Facades\Excel;
use Mtc\Reports\Contracts\ReportModel;
use Mtc\Reports\Http\Requests\UpdateReportRequest;
use Mtc\Reports\Mail\ReportMail;

class ReportEngineRepository extends \Mtc\Reports\ReportRepository
{
    use AppliesQueryConditions;

    protected string $fileName;

    /**
     * Update a report
     *
     * @param Request $request
     * @param ReportModel $report
     * @return ReportModel
     */
    public function update(Request $request, ReportModel $report)
    {
        $this->report = $report;

        $this->report->fill([
            'name' => $request->input('name'),
            'type' => $request->input('type'),
            'active' => $request->input('active'),
            'export_time' => $request->input('export_time'),
            'export_day_of_week' => $request->input('export_day_of_week'),
            'export_day_of_month' => $request->input('export_day_of_month'),
            'next_report_due_at' => $request->input('next_report_due_at'),
            'schedule' => $request->input('schedule'),
            'columns' => $request->input('columns'),
            'recipient' => is_array($request->input('recipient'))
                ? implode(',', $request->input('recipient'))
                : $request->input('recipient'),
            'format' => $request->input('format'),
            'conditions' => $request->input('conditions', []),
        ]);

        $this->setSchedule();
        $this->report->save();

        return $this->report;
    }

    /**
     * View details of a report
     *
     * @param ReportModel $report
     * @return ReportModel
     */
    public function show(ReportModel $report): ReportModel
    {
        $report->load('logs');
        $report->data_options = [
            'types' => collect(config('reports.types', []))
                ->map(fn ($type, $key) => [
                    'id' => $key,
                    'name' => App::make($type)->name(),
                ])->values(),
            'schedules' => collect(config('reports.schedule_types', []))
                ->map(fn ($type, $key) => [
                    'id' => $key,
                    'name' => __('reports.schedule_types.' . $key),
                ])->values(),
            'export_formats' => collect(config('reports.export_formats', []))
                ->map(fn ($type) => [
                    'id' => $type,
                    'name' => $type,
                ])->values(),
            'operators' => collect(config('reports.operators', []))
                ->map(fn ($type) => [
                    'id' => $type,
                    'name' => __('reports.operators.' . $type),
                ])->values(),
            'day_of_week' => collect(range(1, 7))
                ->map(fn ($day) => [
                    'id' => $day,
                    'name' => __('reports.day_of_week.' . $day),
                ])->values(),
            'day_of_month' => collect(range(1, 28))
                ->map(fn ($day) => [
                    'id' => $day,
                    'name' => $day,
                ])->values(),
        ];

        if ($report->type) {
            $report->available_fields = collect(App::make(config('reports.types.' . $report->type))->columns())
                ->map(fn ($name, $id) => [
                    'id' => $id,
                    'name' => $name,
                ])->values();
        }

        $report->columns = $report->columns ?? [];
        $report->conditions = $report->conditions ?? [];
        return $report;
    }

    public function getQuery()
    {
        return $this->buildQuery()->query;
    }

    /**
     * Start eloquent query
     *
     * @return $this
     */
    protected function buildQuery()
    {
        $this->query = App::make(Config::get('reports.types.' . $this->report->type))->query();

        $this->query->with($this->relationshipsInColumns());
        $this->applyQueryConditions($this->query, $this->report->conditions);

        return $this;
    }

    /**
     * Check which relationships should be eager loaded
     *
     * @return array
     */
    protected function relationshipsInColumns(): array
    {
        return collect($this->report->columns)
            ->filter(fn ($column) => str_contains($column, '.') && !Str::startsWith($column, 'value'))
            ->map(function ($column) {
                $split = explode('.', $column);
                array_pop($split);
                return implode('.', $split);
            })
            ->toArray();
    }

    /**
     * Get report data
     *
     * @return $this
     * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
     */
    protected function getData()
    {
        $this->data = $this->mapEntries($this->query->get());
        if ($this->data->isEmpty()) {
            return $this;
        }
        if ($this->report->sendAsFile()) {
            $this->generateAttachment();
        }
        return $this;
    }

    /**
     * Generate attachment for export
     *
     * @return void
     * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
     */
    protected function generateAttachment()
    {
        $this->fileName = Str::slug($this->report->name . Carbon::now()->format('Y-m-d-H-i'))
            . '.' . $this->report->format;
        $exportClass = $this->report->exportClass();
        Excel::store(
            new $exportClass($this->data, $this->report),
            $this->fileName,
            config('reports.report-storage-disk'),
            null,
            ['visibility' => 'private']
        );

        $this->attachment = Attachment::fromData(
            fn() => Storage::disk(config('reports.report-storage-disk'))->get($this->fileName),
            $this->fileName
        );
    }

    /**
     * Sent report via email
     *
     * @return bool
     */
    protected function notify(): bool
    {
        if ($this->data->isNotEmpty()) {
            $recipients = collect(explode(',', $this->report->recipient))
                ->map(fn($recipient) => trim($recipient))
                ->filter(fn($recipient) => $recipient && filter_var($recipient, FILTER_VALIDATE_EMAIL))
                ->each(fn($recipient) => Mail::to($recipient)
                    ->send(new ReportMail($this->data, $this->report, $this->attachment)));
            $this->cleanupAttachment();
        }
        return true;
    }


    /**
     * Map data entries to selected columns
     *
     * @param Collection $data
     * @return Collection
     */
    protected function mapEntries(Collection $data): Collection
    {
        return $data->map(function ($entry) {
            return collect($this->report->columns)
                ->flip()
                ->map(
                    fn($value, $column) => $entry->getAttribute($column, true)
                        ?? $this->getCustomAttribute($entry, $column)
                );
        });
    }

    /**
     * Get custom attribute (relationship or count of relationship entries
     *
     * @param Model $entry
     * @param string $column
     * @return mixed|string
     */
    protected function getCustomAttribute(Model $entry, string $column)
    {
        $split = explode('.', $column);
        $field = array_pop($split);

        while (!empty($split) && !empty($entry)) {
            $relationship = array_shift($split);
            $entry = $entry[$relationship];
        }

        if (empty($entry)) {
            return '';
        }

        if ($field === 'count') {
            return $entry->count();
        }

        return $entry->getAttribute($field);
    }

    /**
     * Set the schedule for next report
     *
     * @return void
     */
    protected function setSchedule()
    {
        if ($this->report->active !== true) {
            $this->report->next_report_due_at = null;
        }

        if ($this->report->isDirty('next_report_due_at')) {
            return;
        }

        $scheduleColumns = [
            'schedule',
            'export_time',
            'export_day_of_week',
            'export_day_of_month',
        ];
        if ($this->report->isDirty($scheduleColumns)) {
            $this->report->next_report_due_at = $this->report->getNextDueAt(Carbon::now());
        }
    }

    private function cleanupAttachment(): void
    {
        if (!empty($this->fileName)) {
            Storage::disk(config('reports.report-storage-disk'))->delete($this->fileName);
        }
    }
}
