<?php

namespace App;

use App\Models\ExportMap;
use Carbon\Carbon;
use Exception;
use Illuminate\Contracts\Filesystem\FileNotFoundException;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Str;
use Maatwebsite\Excel\Facades\Excel;
use Mtc\ContentManager\Models\MediaUse;
use Mtc\MercuryDataModels\SalesChannelHistory;

class ExportMapRepository
{
    public function __construct(protected ExportMap $export_map)
    {
        //
    }

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

    /**
     * Apply query conditions (nested)
     *
     * @param Builder $query
     * @param $conditions
     * @return void
     */
    protected function applyQueryConditions(Builder $query, $conditions): void
    {
        $conditions = collect($conditions)
            ->map(fn($condition) => $condition['value']);

        foreach ($conditions as $condition) {
            if ($this->isRelationshipCondition($condition)) {
                $this->handleRelationshipCondition($query, $condition);
                continue;
            }
            if (Str::endsWith($condition['column'], '_since')) {
                $condition['column'] = str_replace('_since', '', $condition['column']);
                $condition['value'] = Carbon::now()->subDays($condition['value']);
                $condition['operator'] = $this->invertComparisonOperator($condition['operator']);
            }

            match ($condition['type']) {
                'and' => $query->where($condition['column'], $condition['operator'], $condition['value'])
                    ->when(empty($condition['value']), fn($query) => $query->orWhereNull($condition['column'])),
                'or' => $query->orWhere($condition['column'], $condition['operator'], $condition['value']),
                'has' => $query->whereHas($condition['column']),
                'doesnt-have' => $query->whereDoesntHave($condition['column']),
                'null' => $query->whereNull($condition['column']),
                'or-null' => $query->orWhereNull($condition['column']),
                'not-null' => $query->whereNotNull($condition['column']),
                'or-not-null' => $query->orWhereNotNull($condition['column']),
                'where-sub' => $query->where(
                    fn($query) => $this->applyQueryConditions($query, $conditions['conditions'])
                ),
                'or-where-sub' => $query->orWhere(
                    fn($query) => $this->applyQueryConditions($query, $conditions['conditions'])
                ),
            };
        }
    }

    /**
     * Check if the condition is for relationship object
     * Related objects are joined via . (dot)
     *
     * @param $condition
     * @return bool
     */
    protected function isRelationshipCondition(array $condition): bool
    {
        return strpos($condition['column'], '.') > 0;
    }

    protected function handleRelationshipCondition(Builder $query, array $condition)
    {
        $split = explode('.', $condition['column']);

        $last = array_pop($split);
        $relationship = implode('.', $split);

        if ($last === 'count') {
            $relationshipName = strtolower(preg_replace('/(?<!^)[A-Z]/', '_$0', $relationship)) . '_count';
            $query->withCount($relationship)
                ->having($relationshipName, $condition['operator'], $condition['value']);
        } else {
            $query->whereHas($relationship, fn($related) => $related->where(
                $condition['column'],
                $condition['operator'],
                $condition['value']
            ));
        }
    }

    /**
     * Start eloquent query
     *
     * @return $this
     */
    protected function buildQuery()
    {
        $this->query = App::make(Config::get('export_maps.types.' . $this->export_map->type))->query()
            ->latest()
            ->limit(5000);
        $this->query->with($this->relationshipsInColumns());
        $this->applyQueryConditions($this->query, $this->export_map->conditions);

        return $this;
    }

    /**
     * Check which relationships should be eager loaded
     *
     * @return array
     */
    protected function relationshipsInColumns(): array
    {
        return collect($this->export_map->columns)
            ->filter(fn($column) => (str_contains($column, '.') && !Str::startsWith(
                $column,
                'value'
            )) || $column == 'images')
            ->map(function ($column) {
                if ($column == 'images') {
                    return 'mediaUses.media';
                }

                $split = explode('.', $column);
                array_pop($split);
                return implode('.', $split);
            })
            ->toArray();
    }

    /**
     * Get report data
     *
     * @throws FileNotFoundException
     */
    protected function getData()
    {
        $this->data = $this->mapEntries($this->query->get());

        return $this;
    }

    /**
     * Send export file
     *
     */
    protected function sendViaFpt($export)
    {
        try {
            Config::set('filesystems.disks.export-map', [
                'driver' => $this->export_map->driver,
                'host' => $this->export_map->host,
                'root' => $this->export_map->root,
                'username' => $this->export_map->username,
                'password' => $this->export_map->password,
                'filename' => $this->export_map->filename,
            ]);
            Excel::store(
                $export,
                $this->export_map->filename,
                'export-map',
            );

            SalesChannelHistory::store(
                $this->export_map->name,
                true,
                $this->data->count() . ' records exported'
            );
        } catch (Exception $exception) {
            SalesChannelHistory::store($this->export_map->name, false, $exception->getMessage());
        }
    }

    /**
     * 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->export_map->columns)
                ->mapWithKeys(
                    fn($field) => [
                        $field => is_bool($value = $entry->getAttribute($field, true)
                            ?? $this->getCustomAttribute($entry, $field))
                            ? ($value ? 'Y' : 'N')
                            : $value
                    ]
                );
        });
    }

    /**
     * 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 = null)
    {
        if (empty($column)) {
            return '';
        }

        if ($column == 'images') {
            return $entry->mediaUses
                ->filter(fn($mediaUse) => $mediaUse->media)
                ->map(fn(MediaUse $mediaUse) => $mediaUse->getUrl('large'))
                ->implode(',');
        }

        $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();
        }

        if ($entry instanceof Collection) {
            return $entry->pluck($field)->implode(',');
        } elseif ($entry instanceof Model) {
            return $entry->getAttribute($field);
        }

        return '';
    }

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

        if ($this->export_map->isDirty('next_due_at')) {
            return;
        }

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

    private function invertComparisonOperator(string $operator): string
    {
        return match ($operator) {
            '>=' => '<=',
            '>' => '<',
            '<=' => '>=',
            '<' => '>',
            default => $operator,
        };
    }

    /**
     * Get due maps
     *
     * @return Collection
     */
    public function getDueMaps(): Collection
    {
        return $this->export_map->newQuery()
            ->active(1)
            ->where('next_due_at', '<=', Carbon::now())
            ->get();
    }

    /**
     * Run a export map
     * Get data, send and update next shedule time
     *
     * @param ExportMap $exportMap
     * @return void
     */
    public function run(ExportMap $exportMap)
    {
        $this->export_map = $exportMap;

        $export = $this->buildQuery()
            ->getData()
            ->getExport();

        if ($exportMap->ftp_export_enabled) {
            $this->sendViaFpt($export);

            $this->export_map->update([
                'next_due_at' => $this->export_map->getNextDueAt(Carbon::now()),
                'last_sent_at' => Carbon::now(),
            ]);
        }
    }

    public function download($filename = '')
    {
        $export = $this->buildQuery()
            ->getData()
            ->getExport();

        return Excel::download(
            $export,
            $filename,
        );
    }

    private function getExport()
    {
        $exportClass = $this->export_map->exportClass();
        return (new $exportClass($this->data, $this->export_map));
    }
}
