<?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\Traits\StockSyncTraits;
use App\Traits\TracksImportStats;
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;

    /**
     * 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();
        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());
                } else {
                    $this->markVehiclesToBeSynced();
                    $this->loadRemoteFiles();
                }
                Event::dispatch(new StockSyncFinished($this->getProviderName()));
            } catch (\Exception $exception) {
                Log::error('Failed to run import map ' . $import_map->name . ' for ' . tenant('id'), [
                    $exception->getMessage()
                ]);
            }
        }
    }

    /**
     * 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(): void
    {
        $remoteDisk = $this->configureStorageDisk();

        $files = collect($remoteDisk->files())
            ->filter(fn ($file) => $this->isImportableFile($file))
            ->each(function ($file) use ($remoteDisk) {
                $stream = $remoteDisk->readStream($file);
                Storage::disk('local')->writeStream($file, $stream);
                if (is_resource($stream)) {
                    fclose($stream);
                }
            })
            ->each(fn ($file) => $this->executeImport($file))
            ->each(fn ($file) => Storage::disk('local')->delete($file));

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

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

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