<?php

namespace App;

use AllowDynamicProperties;
use App\Models\ExportMap;
use App\Traits\ExportMapCheckRules;
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;
use Mtc\MercuryDataModels\Vehicle;
use Mtc\MercuryDataModels\VehicleOffer;

#[AllowDynamicProperties] class ExportMapRepository
{
    use ExportMapCheckRules;

    private array $imageColumns = [
        'images',
        'images20',
    ];

    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 ($condition['column'] === 'image_count') {
                $query->has('mediaUses', $condition['operator'], (int) $condition['value']);
                continue;
            }
            if ($condition['column'] === 'finance_examples_count') {
                $query->has('financeExamples', $condition['operator'], (int) $condition['value']);
                continue;
            }
            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(function ($subQuery) use ($condition) {
                    $operator = $condition['operator'];
                    $column = $condition['column'];
                    $value = $condition['value'];

                    $subQuery
                        ->when($operator === 'in', function ($q) use ($column, $value) {
                            $q->whereIn($column, array_map(trim(...), explode(',', $value ?? '')));
                        })
                        ->when($operator === 'not_in', function ($q) use ($column, $value) {
                            $q->whereNotIn($column, array_map(trim(...), explode(',', $value ?? '')));
                        })
                        ->when(!in_array($operator, ['in', 'not_in']), function ($q) use ($column, $operator, $value) {
                            $q->where($column, $operator, $value)
                                ->when(empty($value), fn($innerQuery) => $innerQuery->orWhereNull($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((string) $condition['column'], '.') > 0;
    }

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

        $last = array_pop($split);
        $relationship = implode('.', $split);
        $arrayValue = array_map(trim(...), explode(',', (string) $condition['value']));

        if ($last === 'count') {
            $relationshipName = strtolower((string) preg_replace('/(?<!^)[A-Z]/', '_$0', $relationship)) . '_count';
            $query->withCount($relationship)
                ->having($relationshipName, $condition['operator'], $condition['value']);
        } elseif ($condition['operator'] === 'in') {
            $query->whereHas($relationship, fn($related) => $related->whereIn($last, $arrayValue));
        } elseif ($condition['operator'] === 'not_in') {
            $query->whereHas($relationship, fn($related) => $related->whereNotIn($last, $arrayValue));
        } else {
            $query->whereHas($relationship, fn($related) => $related->where(
                $last,
                $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
    {
        $model = App::make(Config::get('export_maps.types.' . $this->export_map->type))
            ->query()
            ->getModel();

        return collect($this->export_map->columns)
            ->filter(fn($column) => (str_contains((string) $column, '.') && !Str::startsWith(
                $column,
                'value'
            )) || $this->isImageColumn($column) || $column === 'image_count' || $column === 'finance_examples_count')
            ->map(function ($column) use ($model) {
                if ($this->isImageColumn($column) || $column === 'image_count') {
                    return 'mediaUses.media';
                }

                if ($column === 'finance_examples_count') {
                    return 'financeExamples';
                }

                if (Str::startsWith($column, 'attribute.')) {
                    return 'attributeValues';
                }

                $split = explode('.', $column);
                array_pop($split);

                // Walk segments and only include those that are actual relationships
                $relationshipPath = [];
                $currentModel = $model;

                foreach ($split as $segment) {
                    if (
                        $currentModel && method_exists($currentModel, $segment)
                        && $currentModel->$segment() instanceof \Illuminate\Database\Eloquent\Relations\Relation
                    ) {
                        $relationshipPath[] = $segment;
                        $currentModel = $currentModel->$segment()->getRelated();
                    } else {
                        break;
                    }
                }

                return !empty($relationshipPath) ? implode('.', $relationshipPath) : null;
            })
            ->filter()
            ->unique()
            ->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,
                'port' => $this->export_map->port,
                '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
    {
        $columns = $this->export_map->columns ?? [];
        $rules = collect($this->export_map->mapping_rules ?? [])
            ->reject(fn($rule) => !($rule['when'] ?? null) || empty($rule['then'] ?? []));

        return $data->map(function ($entry) use ($columns, $rules) {
            $rowByField = [];
            foreach ($columns as $field) {
                $rowByField[$field] = $this->castForExport($this->getFieldValue($entry, $field));
            }

            foreach ($rules as $rule) {
                if ($this->evaluateCondition($entry, $rowByField, $rule['when'])) {
                    foreach ($rule['then'] as $action) {
                        $target = $action['target'];
                        $value = $this->resolveFieldPlaceholders($action['value'] ?? null, $rowByField);
                        $actionType = $action['action'] ?? 'set';

                        match ($actionType) {
                            'append' => $rowByField[$target] = ($rowByField[$target] ?? '') . ($value ?? ''),
                            'prepend' => $rowByField[$target] = ($value ?? '') . ($rowByField[$target] ?? ''),
                            default => $rowByField[$target] = $value === null ? null : $this->castForExport($value),
                        };
                    }
                }
            }

            $out = [];
            foreach ($columns as $key => $field) {
                $out[$key] = $rowByField[$field] ?? null;
            }

            return $out;
        });
    }


    protected function resolveFieldPlaceholders(?string $value, array $rowByField): ?string
    {
        if ($value === null || !str_contains($value, '{{')) {
            return $value;
        }

        return preg_replace_callback('/\{\{(\w+)\}\}/', function ($matches) use ($rowByField) {
            return $rowByField[$matches[1]] ?? '';
        }, $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) || $column === 'empty') {
            return '';
        }

        if ($column === 'site_url') {
            if ($entry instanceof VehicleOffer) {
                return \App\Facades\Site::offerUrl($entry);
            }
            if ($entry instanceof Vehicle) {
                return \App\Facades\Site::vehicleUrl($entry);
            }
            return '';
        }

        if ($column === 'image_count') {
            return $entry->mediaUses
                ->filter(fn($mediaUse) => $mediaUse->media)
                ->count();
        }

        if ($column === 'finance_examples_count') {
            return $entry->financeExamples->count();
        }

        if ($this->isImageColumn($column)) {
            $sorted = $entry->mediaUses
                ->filter(fn($mediaUse) => $mediaUse->media)
                ->sortBy(fn(MediaUse $mediaUse) => $mediaUse->order);

            if (preg_match('/^image_(\d+)$/', $column, $matches)) {
                $index = (int) $matches[1] - 1;
                $mediaUse = $sorted->values()->get($index);
                return $mediaUse ? $mediaUse->media->getOriginalUrlAttribute(true) : '';
            }

            return $sorted
                ->when($column === 'images20', fn($collection) => $collection->take(20))
                ->map(fn(MediaUse $mediaUse) => $mediaUse->media->getOriginalUrlAttribute(true))
                ->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(',');
        }

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

    private function isImageColumn(string $column): bool
    {
        return in_array($column, $this->imageColumns, true)
            || preg_match('/^image_\d+$/', $column);
    }
}
