<?php
declare(strict_types=1);

namespace Autogedal\EanTools\Model;

use Autogedal\EanTools\Model\Spreadsheet\Format;
use Autogedal\EanTools\Model\Spreadsheet\RowWriterFactory;
use Autogedal\EanTools\Model\Spreadsheet\RowWriterInterface;
use Magento\Framework\App\Filesystem\DirectoryList;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\Filesystem;
use Magento\Framework\Filesystem\Directory\WriteInterface;
use Magento\Framework\Math\Random;
use Magento\Framework\Serialize\Serializer\Json;
use Throwable;

class ResultStorage
{
    public const BASE_PATH = 'autogedal_ean';

    public const STATUS_QUEUED = 'queued';
    public const STATUS_PROCESSING = 'processing';
    public const STATUS_COMPLETE = 'complete';
    public const STATUS_FAILED = 'failed';

    private const RETENTION_DAYS = 7;
    private const RUN_ID_PATTERN = '/^[0-9]{8}-[0-9]{6}-[a-z0-9]{8}$/';

    public function __construct(
        private readonly Filesystem $filesystem,
        private readonly Random $random,
        private readonly Json $json,
        private readonly RowWriterFactory $writerFactory,
        private readonly Format $format
    ) {
    }

    public function createRun(string $tool): string
    {
        $this->purgeOldRuns();

        $suffix = $this->random->getRandomString(8, Random::CHARS_LOWERS . Random::CHARS_DIGITS);
        $runId = date('Ymd-His') . '-' . $suffix;
        $directory = $this->getDirectory();
        $directory->create(self::BASE_PATH . '/' . $runId);

        $this->writeManifest($runId, [
            'tool' => $tool,
            'created_at' => date('Y-m-d H:i:s'),
            'status' => self::STATUS_PROCESSING,
            'files' => [],
            'summary' => [],
            'notes' => [],
        ]);

        return $runId;
    }

    public function queue(string $runId, string $format, array $request = []): void
    {
        $manifest = $this->readManifest($runId);
        $manifest['status'] = self::STATUS_QUEUED;
        $manifest['format'] = $this->format->resolve($format);
        $manifest['request'] = $request;
        $manifest['queued_at'] = gmdate('Y-m-d H:i:s');
        unset($manifest['started_at'], $manifest['completed_at'], $manifest['error']);
        $this->writeManifest($runId, $manifest);
    }

    public function markProcessing(string $runId): bool
    {
        $manifest = $this->readManifest($runId);
        if (($manifest['status'] ?? null) !== self::STATUS_QUEUED) {
            return false;
        }

        $manifest['status'] = self::STATUS_PROCESSING;
        $manifest['started_at'] = gmdate('Y-m-d H:i:s');
        $this->writeManifest($runId, $manifest);

        return true;
    }

    public function getNextQueuedRunId(): ?string
    {
        $directory = $this->getDirectory();
        if (!$directory->isExist(self::BASE_PATH)) {
            return null;
        }

        $runIds = [];
        foreach ($directory->read(self::BASE_PATH) as $entry) {
            $runId = substr($entry, strlen(self::BASE_PATH) + 1);
            if ($this->isRunId($runId) && $directory->isDirectory($entry)) {
                $runIds[] = $runId;
            }
        }

        sort($runIds);
        foreach ($runIds as $runId) {
            try {
                if (($this->readManifest($runId)['status'] ?? null) === self::STATUS_QUEUED) {
                    return $runId;
                }
            } catch (Throwable) {
                continue;
            }
        }

        return null;
    }

    public function storeInputFile(string $runId, string $absolutePath, string $originalName): string
    {
        $this->assertRunId($runId);
        $extension = $this->format->fromFileName($originalName) === Format::XLSX ? 'xlsx' : 'csv';

        $directory = $this->getDirectory();
        $source = $directory->getRelativePath($absolutePath);
        $uploadPath = self::BASE_PATH . '/_uploads/';
        if (!str_starts_with($source, $uploadPath) || !$directory->isFile($source)) {
            throw new LocalizedException(__('The uploaded file is no longer available.'));
        }

        $fileName = 'input.' . $extension;
        $directory->renameFile($source, self::BASE_PATH . '/' . $runId . '/' . $fileName);

        return $fileName;
    }

    public function getInputFilePath(string $runId, string $fileName): string
    {
        $this->assertRunId($runId);
        if (!preg_match('/^input\.(csv|txt|xlsx)$/', $fileName)) {
            throw new LocalizedException(__('Invalid input file.'));
        }

        $path = self::BASE_PATH . '/' . $runId . '/' . $fileName;
        $directory = $this->getDirectory();
        if (!$directory->isFile($path)) {
            throw new LocalizedException(__('The uploaded file is no longer available.'));
        }

        return $directory->getAbsolutePath($path);
    }

    public function removeInputFile(string $runId, string $fileName): void
    {
        try {
            $path = $this->getInputFilePath($runId, $fileName);
            $directory = $this->getDirectory();
            $directory->delete($directory->getRelativePath($path));
        } catch (Throwable) {
            return;
        }
    }

    public function buildFileName(string $baseName, string $format): string
    {
        return $baseName . '.' . $this->format->getExtension($format);
    }

    public function openWriter(string $runId, string $fileName, array $header, string $format): RowWriterInterface
    {
        $this->assertRunId($runId);
        $this->assertFileName($fileName);

        $absolute = $this->getDirectory()->getAbsolutePath(
            self::BASE_PATH . '/' . $runId . '/' . $fileName
        );

        return $this->writerFactory->create($absolute, $format, $header);
    }

    public function registerFile(
        string $runId,
        string $fileName,
        string $label,
        int $rows,
        string $tone = 'neutral'
    ): void {
        $manifest = $this->readManifest($runId);
        $manifest['files'][] = [
            'name' => $fileName,
            'label' => $label,
            'rows' => $rows,
            'tone' => $tone,
            'size' => $this->getFileSize($runId, $fileName),
        ];
        $this->writeManifest($runId, $manifest);
    }

    public function finish(string $runId, array $summary, array $notes = []): void
    {
        $manifest = $this->readManifest($runId);
        $manifest['status'] = self::STATUS_COMPLETE;
        $manifest['summary'] = $summary;
        $manifest['notes'] = $notes;
        $manifest['completed_at'] = gmdate('Y-m-d H:i:s');
        unset($manifest['error']);
        $this->writeManifest($runId, $manifest);
    }

    public function fail(string $runId, string $message): void
    {
        $manifest = $this->readManifest($runId);
        $manifest['status'] = self::STATUS_FAILED;
        $manifest['error'] = $message;
        $manifest['files'] = [];
        $manifest['summary'] = [];
        $manifest['notes'] = [];
        $manifest['completed_at'] = gmdate('Y-m-d H:i:s');
        $this->deleteRunFiles($runId);
        $this->writeManifest($runId, $manifest);
    }

    public function readManifest(string $runId): array
    {
        $this->assertRunId($runId);
        $path = self::BASE_PATH . '/' . $runId . '/manifest.json';
        $directory = $this->getDirectory();

        if (!$directory->isExist($path)) {
            throw new LocalizedException(__('The requested result no longer exists. Run the tool again.'));
        }

        $data = $this->json->unserialize($directory->readFile($path));

        return is_array($data) ? $data : [];
    }

    public function listRuns(): array
    {
        $directory = $this->getDirectory();
        if (!$directory->isExist(self::BASE_PATH)) {
            return [];
        }

        $runs = [];
        foreach ($directory->read(self::BASE_PATH) as $entry) {
            $runId = substr($entry, strlen(self::BASE_PATH) + 1);
            if (!$this->isRunId($runId) || !$directory->isDirectory($entry)) {
                continue;
            }

            try {
                $manifest = $this->readManifest($runId);
                $mtime = (int)$directory->stat($entry)['mtime'];
            } catch (Throwable) {
                continue;
            }

            $runs[] = [
                'run_id' => $runId,
                'tool' => (string)($manifest['tool'] ?? ''),
                'created_at' => (string)($manifest['created_at'] ?? ''),
                'status' => (string)($manifest['status'] ?? self::STATUS_COMPLETE),
                'error' => isset($manifest['error']) ? (string)$manifest['error'] : null,
                'expires_at' => $mtime + (self::RETENTION_DAYS * 86400),
                'files' => $this->readRunFiles($runId, (array)($manifest['files'] ?? [])),
            ];
        }

        usort($runs, static fn (array $a, array $b): int => strcmp($b['run_id'], $a['run_id']));

        return $runs;
    }

    public function getRetentionDays(): int
    {
        return self::RETENTION_DAYS;
    }

    public function getRelativeFilePath(string $runId, string $fileName): string
    {
        $this->assertRunId($runId);
        $this->assertFileName($fileName);

        $path = self::BASE_PATH . '/' . $runId . '/' . $fileName;
        if (!$this->getDirectory()->isExist($path)) {
            throw new LocalizedException(__('The requested file no longer exists. Run the tool again.'));
        }

        return $path;
    }

    public function fileExists(string $runId, string $fileName): bool
    {
        try {
            $this->assertRunId($runId);
            $this->assertFileName($fileName);
        } catch (LocalizedException) {
            return false;
        }

        return $this->getDirectory()->isExist(self::BASE_PATH . '/' . $runId . '/' . $fileName);
    }

    public function purgeOldRuns(): void
    {
        $directory = $this->getDirectory();
        if (!$directory->isExist(self::BASE_PATH)) {
            return;
        }

        $cutoff = time() - (self::RETENTION_DAYS * 86400);

        foreach ($directory->read(self::BASE_PATH) as $entry) {
            $runId = substr($entry, strlen(self::BASE_PATH) + 1);
            if (!$this->isRunId($runId) || !$directory->isDirectory($entry)) {
                continue;
            }

            try {
                if ($directory->stat($entry)['mtime'] < $cutoff) {
                    $directory->delete($entry);
                }
            } catch (Throwable) {
                continue;
            }
        }
    }

    private function readRunFiles(string $runId, array $files): array
    {
        $existing = [];

        foreach ($files as $file) {
            if (!is_array($file) || !isset($file['name'])) {
                continue;
            }

            $fileName = (string)$file['name'];
            if (!$this->fileExists($runId, $fileName)) {
                continue;
            }

            $existing[] = [
                'name' => $fileName,
                'label' => (string)($file['label'] ?? $fileName),
                'rows' => (int)($file['rows'] ?? 0),
                'tone' => (string)($file['tone'] ?? 'neutral'),
                'size' => $this->getFileSize($runId, $fileName),
            ];
        }

        return $existing;
    }

    private function getFileSize(string $runId, string $fileName): int
    {
        try {
            return (int)$this->getDirectory()->stat(self::BASE_PATH . '/' . $runId . '/' . $fileName)['size'];
        } catch (Throwable) {
            return 0;
        }
    }

    private function deleteRunFiles(string $runId): void
    {
        $directory = $this->getDirectory();
        $path = self::BASE_PATH . '/' . $runId;

        foreach ($directory->read($path) as $entry) {
            if (substr($entry, strlen($path) + 1) === 'manifest.json') {
                continue;
            }

            try {
                $directory->delete($entry);
            } catch (Throwable) {
                continue;
            }
        }
    }

    private function writeManifest(string $runId, array $manifest): void
    {
        $this->getDirectory()->writeFile(
            self::BASE_PATH . '/' . $runId . '/manifest.json',
            $this->json->serialize($manifest)
        );
    }

    private function assertRunId(string $runId): void
    {
        if (!$this->isRunId($runId)) {
            throw new LocalizedException(__('Invalid run identifier.'));
        }
    }

    private function isRunId(string $runId): bool
    {
        return (bool)preg_match(self::RUN_ID_PATTERN, $runId);
    }

    private function assertFileName(string $fileName): void
    {
        if (!preg_match('/^[a-z0-9_]+\.(csv|xlsx|json)$/', $fileName)) {
            throw new LocalizedException(__('Invalid file name.'));
        }
    }

    private function getDirectory(): WriteInterface
    {
        return $this->filesystem->getDirectoryWrite(DirectoryList::VAR_DIR);
    }
}
