<?php

namespace App\Modules\Stock;

use App\Contracts\StockProvider;
use App\Events\StockSyncFinished;
use App\Facades\Settings;
use App\Imports\Traits\VehicleImportFlow;
use App\Imports\VehicleImport;
use App\Imports\VehicleImportWithoutHeadingRow;
use App\Models\ImportMap as ImportMapModel;
use App\Models\ImportMapFile;
use App\Traits\StockSyncTraits;
use App\Traits\TracksImportStats;
use Carbon\Carbon;
use Illuminate\Contracts\Filesystem\Filesystem;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Maatwebsite\Excel\Facades\Excel;
use Symfony\Component\Finder\SplFileInfo;
use ZipArchive;

class ImportMap implements StockProvider
{
    use VehicleImportFlow;
    use StockSyncTraits;
    use TracksImportStats;

    private array $config = [];

    private ?ImportMapModel $import_map = null;

    private int $processedFileCount = 0;

    /**
     * Check if enabled
     *
     * @return bool
     */
    public function enabled(): bool
    {
        return Settings::get('stock-file-enabled') ?? false;
    }

    /**
     * Name of the integration
     *
     * @return string
     */
    public function name(): string
    {
        return 'Remote stock file';
    }

    /**
     * Perform a scheduled import task
     *
     * @return void
     */
    public function runScheduledImport(bool $fullSync = true): void
    {
        $this->resetStats();
        $this->processedFileCount = 0;
        Config::set('excel.transactions.handler', 'null');
        $runners = $this->loadImportMaps();
        foreach ($runners as $import_map) {
            try {
                $this->import_map = $import_map;

                if (($import_map->data['source_type'] ?? 'file') === HttpJsonImportMap::SOURCE_TYPE) {
                    $httpOrchestrator = app(HttpJsonImportMap::class);
                    $httpOrchestrator->runForMap($import_map);
                    $this->aggregateStats($httpOrchestrator->getImportStats());
                    Event::dispatch(new StockSyncFinished($this->getProviderName()));
                } else {
                    $processed = $this->loadRemoteFiles();
                    $this->processedFileCount += $processed;
                    // Only fire downstream sync work when a changed file was actually processed.
                    if ($processed > 0) {
                        Event::dispatch(new StockSyncFinished($this->getProviderName()));
                    }
                }
            } catch (\Exception $exception) {
                Log::error('Failed to run import map ' . $import_map->name . ' for ' . tenant('id'), [
                    $exception->getMessage()
                ]);
            }
        }
    }

    /**
     * Number of remote files (re)imported during the last scheduled run.
     */
    public function getProcessedFileCount(): int
    {
        return $this->processedFileCount;
    }

    /**
     * Fields to add to dealership management
     *
     * @return array[]
     */
    public function dealershipAdditionalDataFields(): array
    {
        return [
            'custom-feed-location-id' => [
                'type' => 'text',
                'label' => 'Dealership ID on stock feed',
            ],
        ];
    }

    private function loadImportMaps(): Collection
    {
        return ImportMapModel::query()
            ->get()
            ->filter(fn (ImportMapModel $map) => !empty($map->data['automate']));
    }

    private function loadRemoteFiles(): int
    {
        $remoteDisk = $this->configureStorageDisk();

        $dueFiles = collect($remoteDisk->files())
            ->filter(fn ($file) => $this->isImportableFile($file))
            ->mapWithKeys(fn ($file) => [$file => $this->remoteModifiedTime($remoteDisk, $file)])
            ->filter(fn ($modifiedAt, $file) => $this->fileNeedsImport($file, $modifiedAt));

        if ($dueFiles->isEmpty()) {
            return 0;
        }

        // Only flag vehicles for the unsynced sweep once we have a changed file to process -
        // otherwise a quiet run would clear was_recently_synced without importing anything.
        $this->markVehiclesToBeSynced();

        $dueFiles->each(function ($modifiedAt, $file) use ($remoteDisk) {
            $stream = $remoteDisk->readStream($file);
            Storage::disk('local')->writeStream($file, $stream);
            if (is_resource($stream)) {
                fclose($stream);
            }

            $this->executeImport($file);
            $this->recordImportedFile($file, $modifiedAt);

            Storage::disk('local')->delete($file);
        });

        if ($this->import_map['data']['remove_from_remote'] ?? false) {
            $dueFiles->each(fn ($modifiedAt, $file) => $remoteDisk->move($file, $this->getBackupFileName($file)));
        }

        if (!empty($this->import_map->data['clear_previous_entries'])) {
            $this->removeNotSynced(false);
        }

        return $dueFiles->count();
    }

    /**
     * Decide whether a remote file should be (re)imported based on its modified time.
     *
     * Skips files that have not changed since the last import, and files modified too
     * recently (still potentially being uploaded / in-flight).
     */
    private function fileNeedsImport(string $file, ?Carbon $modifiedAt): bool
    {
        // Modified time unavailable - process it to preserve the previous always-import
        // behaviour rather than silently skipping potential updates.
        if ($modifiedAt === null) {
            return true;
        }

        $minimumAge = (int) config('automotive.import_map_min_file_age', 180);
        if ($modifiedAt->greaterThan(Carbon::now()->subSeconds($minimumAge))) {
            return false;
        }

        $lastImported = $this->lastImportedModifiedTime($file);

        return $lastImported === null || $modifiedAt->greaterThan($lastImported);
    }

    private function remoteModifiedTime(Filesystem $disk, string $file): ?Carbon
    {
        try {
            return Carbon::createFromTimestamp($disk->lastModified($file));
        } catch (\Throwable $exception) {
            return null;
        }
    }

    private function lastImportedModifiedTime(string $file): ?Carbon
    {
        return ImportMapFile::query()
            ->where('map_id', $this->import_map->id)
            ->where('file_name', $file)
            ->value('file_modified_at');
    }

    private function recordImportedFile(string $file, ?Carbon $modifiedAt): void
    {
        ImportMapFile::query()->updateOrCreate(
            ['map_id' => $this->import_map->id, 'file_name' => $file],
            ['file_modified_at' => $modifiedAt, 'imported_at' => Carbon::now()],
        );
    }

    private function configureStorageDisk(): Filesystem
    {
        return Storage::build([
            'driver' => $this->import_map['data']['protocol'],
            'root' => $this->import_map['data']['file_root'] ?? null,
            'host' => $this->import_map['data']['host'],
            'username' => $this->import_map['data']['username'],
            'password' => $this->import_map['data']['password'],
            'throw' => true,
        ]);
    }

    private function executeImport($file): void
    {
        if (pathinfo($file, PATHINFO_EXTENSION) === 'zip') {
            $this->extractZipArchive(
                Storage::disk('local')->path($file),
                Storage::disk('local')->path($this->getArchiveFolder()),
            );

            /** @var SplFileInfo $stock_file */
            $file = collect(Storage::disk('local')->files($this->getArchiveFolder()))
                ->filter(fn ($filename) => in_array(pathinfo($filename, PATHINFO_EXTENSION), ['csv', 'xls', 'xlsx']))
                ->first();

            if (empty($file)) {
                return;
            }
        }

        if ($this->import_map && empty($this->import_map->data['has_header_row'])) {
            $import = new VehicleImportWithoutHeadingRow($this->import_map->id)
                ->setImportMap($this->import_map, true)
                ->setArchiveFolder($this->getArchiveFolder())
                ->setAdditionalTasks($this->import_map['data']['automations'] ?? []);
        } else {
            $import = new VehicleImport($this->import_map->id)
                ->setImportMap($this->import_map, true)
                ->setArchiveFolder($this->getArchiveFolder())
                ->setAdditionalTasks($this->import_map['data']['automations'] ?? []);
        }
        Excel::import($import, $file, 'local');
        $this->aggregateStats($import->getImportStats());
        $this->removeLocalFiles(Storage::disk('local')->path($this->getArchiveFolder()));
    }

    private function aggregateStats(array $stats): void
    {
        for ($i = 0; $i < $stats['created']; $i++) {
            $this->incrementCreated();
        }
        for ($i = 0; $i < $stats['updated']; $i++) {
            $this->incrementUpdated();
        }
        for ($i = 0; $i < $stats['failed']; $i++) {
            $this->incrementFailed();
        }
    }

    private function extractZipArchive(string $archivePath, string $extractPath): void
    {
        $zip = new ZipArchive();

        if ($zip->open($archivePath) === true) {
            $zip->extractTo($extractPath);
            $zip->close();
        } else {
            throw new \Exception("Unable to extract the ZIP file");
        }
    }

    private function isImportableFile(string $file): bool
    {
        $import_map_filename = $this->import_map['data']['file_name'] ?? null;
        $file_matches = empty($import_map_filename) || ($file == $import_map_filename);
        return $this->isSpecifiedFileFormat($file) && $file_matches;
    }

    private function isSpecifiedFileFormat(string $file): bool
    {
        return in_array(pathinfo($file, PATHINFO_EXTENSION), ['csv', 'xls', 'xlsx', 'zip']);
    }

    private function getBackupFileName(string $file): string
    {
        return $this->import_map->id . '_import.' . pathinfo($file, PATHINFO_EXTENSION) . '.bck';
    }

    private function removeLocalFiles(string $stock_path): void
    {
        File::deleteDirectory($stock_path);
    }
}
