<?php
declare(strict_types=1);

namespace Autogedal\EanTools\Model;

class Normalizer
{
    public const STATUS_OK = 'ok';
    public const STATUS_WARNING = 'warning';
    public const STATUS_ERROR = 'error';
    public const STORED_ISSUE_NON_DIGIT = 'non_digit_characters';
    public const STORED_ISSUE_LENGTH = 'invalid_length';
    public const STORED_ISSUE_CHECK_DIGIT = 'invalid_check_digit';

    private const ISSUE_EMPTY = 'empty';
    private const ISSUE_FORMULA_WITHOUT_RESULT = 'formula_without_cached_result';
    private const ISSUE_SCIENTIFIC_NOTATION = 'scientific_notation';
    private const ISSUE_NO_DIGITS = 'no_digits';
    private const ISSUE_UNEXPECTED_LENGTH = 'unexpected_length';
    private const ISSUE_CLEANED = 'cleaned';
    private const ISSUE_DUPLICATE_IN_FILE = 'duplicate_in_file';

    private const EAN_LENGTH = 13;

    public function normalize(string $raw): array
    {
        $original = $raw;

        $clean = str_replace(["\xEF\xBB\xBF", "\xC2\xA0"], '', $raw);
        $clean = trim($clean);

        if (preg_match('/^="([^"]*)"$/', $clean, $literal) === 1) {
            $clean = $literal[1];
        }

        $clean = trim($clean);
        $clean = trim($clean, "\"'");
        $clean = trim($clean);

        if ($clean === '') {
            return $this->result(
                $original,
                '',
                self::STATUS_ERROR,
                self::ISSUE_EMPTY,
                (string)__('The cell is empty.')
            );
        }

        if (str_starts_with($clean, '=')) {
            return $this->result(
                $original,
                '',
                self::STATUS_ERROR,
                self::ISSUE_FORMULA_WITHOUT_RESULT,
                (string)__(
                    'The cell contains a formula whose cached result is not available in the file, so the code cannot be read. Open the file, replace the formulas with their values (Copy, then Paste Special > Values), and upload it again.'
                )
            );
        }

        if (preg_match('/^[0-9]+(?:[.,][0-9]+)?[eE]\+?[0-9]+$/', $clean)) {
            return $this->result(
                $original,
                '',
                self::STATUS_ERROR,
                self::ISSUE_SCIENTIFIC_NOTATION,
                (string)__('Excel converted the code to scientific notation, so the original digits can no longer be read from the file. Re-export the CSV with the column formatted as Text and upload it again.')
            );
        }

        $digits = (string)preg_replace('/\D+/', '', $clean);
        if ($digits === '') {
            return $this->result(
                $original,
                '',
                self::STATUS_ERROR,
                self::ISSUE_NO_DIGITS,
                (string)__('The value does not contain any digits.')
            );
        }

        $strippedSomething = $digits !== $clean;
        $length = strlen($digits);

        if ($length !== self::EAN_LENGTH) {
            return $this->result(
                $original,
                $digits,
                self::STATUS_WARNING,
                self::ISSUE_UNEXPECTED_LENGTH,
                (string)__(
                    'Length %1; EAN codes used in the catalog have %2 digits. If the file was saved from Excel, check whether leading zeros were lost.',
                    $length,
                    self::EAN_LENGTH
                )
            );
        }

        if (!$this->hasValidCheckDigit($digits)) {
            return $this->result(
                $original,
                $digits,
                self::STATUS_WARNING,
                self::STORED_ISSUE_CHECK_DIGIT,
                (string)__('The last digit (GS1 check digit) does not match. The code is probably incorrect or incomplete.')
            );
        }

        if ($strippedSomething) {
            return $this->result(
                $original,
                $digits,
                self::STATUS_OK,
                self::ISSUE_CLEANED,
                (string)__('Non-numeric characters (spaces, hyphens, etc.) were removed.')
            );
        }

        return $this->result($original, $digits, self::STATUS_OK, '', '');
    }

    public function normalizeList(array $rows): array
    {
        $accepted = [];
        $rejected = [];
        $warnings = [];
        $duplicatesInFile = [];
        $seen = [];

        foreach ($rows as $row) {
            $res = $this->normalize($row['value']);
            $res['line'] = $row['line'];

            if ($res['status'] === self::STATUS_ERROR) {
                $rejected[] = $res;
                continue;
            }

            if (isset($seen[$res['value']])) {
                $dup = $res;
                $dup['message'] = (string)__(
                    'Duplicate in file; the first occurrence is on line %1.',
                    $seen[$res['value']]
                );
                $dup['issue'] = self::ISSUE_DUPLICATE_IN_FILE;
                $duplicatesInFile[] = $dup;
                continue;
            }

            $seen[$res['value']] = $row['line'];

            if ($res['status'] === self::STATUS_WARNING) {
                $warnings[] = $res;
            }

            $accepted[] = $res;
        }

        return [
            'accepted' => $accepted,
            'rejected' => $rejected,
            'warnings' => $warnings,
            'duplicatesInFile' => $duplicatesInFile,
        ];
    }

    public function inspectStored(string $value): array
    {
        $digits = (string)preg_replace('/\D+/', '', $value);

        if ($digits !== $value) {
            return [
                'issue' => self::STORED_ISSUE_NON_DIGIT,
                'message' => (string)__('The stored value contains non-numeric characters (spaces, hyphens, letters, or scientific notation).'),
                'digits' => $digits,
            ];
        }

        $length = strlen($digits);
        if ($length !== self::EAN_LENGTH) {
            return [
                'issue' => self::STORED_ISSUE_LENGTH,
                'message' => (string)__(
                    'Length %1; EAN codes used in the catalog have %2 digits.',
                    $length,
                    self::EAN_LENGTH
                ),
                'digits' => $digits,
            ];
        }

        if (!$this->hasValidCheckDigit($digits)) {
            return [
                'issue' => self::STORED_ISSUE_CHECK_DIGIT,
                'message' => (string)__('The last digit (GS1 check digit) does not match the rest of the code.'),
                'digits' => $digits,
            ];
        }

        return ['issue' => '', 'message' => '', 'digits' => $digits];
    }

    public function getStoredIssueLabels(): array
    {
        return [
            self::STORED_ISSUE_NON_DIGIT => (string)__('Contains non-numeric characters'),
            self::STORED_ISSUE_LENGTH => (string)__('Length differs from %1 digits', self::EAN_LENGTH),
            self::STORED_ISSUE_CHECK_DIGIT => (string)__('Incorrect check digit'),
        ];
    }

    public function getIssueLabel(string $issue): string
    {
        return match ($issue) {
            self::ISSUE_EMPTY => (string)__('Empty cell'),
            self::ISSUE_FORMULA_WITHOUT_RESULT => (string)__('Formula without cached result'),
            self::ISSUE_SCIENTIFIC_NOTATION => (string)__('Scientific notation'),
            self::ISSUE_NO_DIGITS => (string)__('No digits'),
            self::ISSUE_UNEXPECTED_LENGTH => (string)__('Unexpected length'),
            self::STORED_ISSUE_CHECK_DIGIT => (string)__('Invalid check digit'),
            self::ISSUE_CLEANED => (string)__('Cleaned value'),
            self::ISSUE_DUPLICATE_IN_FILE => (string)__('Duplicate in file'),
            self::STORED_ISSUE_NON_DIGIT => (string)__('Non-numeric characters'),
            self::STORED_ISSUE_LENGTH => (string)__('Invalid length'),
            default => $issue,
        };
    }

    public function hasValidCheckDigit(string $digits): bool
    {
        $length = strlen($digits);
        if ($length < 2 || !ctype_digit($digits)) {
            return false;
        }

        $sum = 0;
        for ($i = $length - 2, $weight = 3; $i >= 0; $i--, $weight = $weight === 3 ? 1 : 3) {
            $sum += (int)$digits[$i] * $weight;
        }

        $expected = (10 - ($sum % 10)) % 10;

        return $expected === (int)$digits[$length - 1];
    }

    private function result(string $original, string $value, string $status, string $issue, string $message): array
    {
        return [
            'original' => $original,
            'value' => $value,
            'status' => $status,
            'issue' => $issue,
            'message' => $message,
        ];
    }
}
