<?php

namespace App\Support;

/**
 * CSV statement parser for importing real transaction rows.
 */
final class CsvStatementParser
{
    /** @var list<string> */
    private const DATE_KEYS = ['date', 'data', 'transaction date', 'posted', 'value date', 'datum'];

    /** @var list<string> */
    private const DESC_KEYS = ['description', 'descricao', 'merchant', 'name', 'details', 'memo', 'narration', 'concept'];

    /** @var list<string> */
    private const AMOUNT_KEYS = ['amount', 'valor', 'value', 'total', 'debit', 'credit'];

    /** @var list<string> */
    private const DEBIT_KEYS = ['debit', 'withdrawal', 'out', 'saida', 'expense'];

    /** @var list<string> */
    private const CREDIT_KEYS = ['credit', 'deposit', 'in', 'entrada', 'income'];

    public static function fingerprint(string $date, float $amount, string $name): string
    {
        $norm = mb_strtolower(preg_replace('/\s+/u', ' ', trim($name)) ?? '');

        return $date.'|'.number_format($amount, 2, '.', '').'|'.$norm;
    }

    /**
     * @param  list<array{date: string, amount: float|int, name: string}>  $rows
     * @return array<string, true>
     */
    public static function buildFingerprintsFromRows(array $rows): array
    {
        $set = [];
        foreach ($rows as $row) {
            $amt = (float) ($row['amount'] ?? 0);
            $set[self::fingerprint((string) ($row['date'] ?? ''), $amt, (string) ($row['name'] ?? ''))] = true;
        }

        return $set;
    }

    /**
     * @param  array<string, true>  $existingFingerprints
     * @return list<array<string, mixed>>
     */
    public static function parseCsv(string $text, array $existingFingerprints = []): array
    {
        $lines = array_values(array_filter(preg_split("/\r\n|\n|\r/", $text) ?: [], fn ($l) => trim((string) $l) !== ''));
        if (count($lines) < 2) {
            return [];
        }

        $delimiter = self::detectDelimiter($lines[0]);
        $splitLine = function (string $line) use ($delimiter): array {
            if ($delimiter === ',') {
                return self::splitCsvLine($line);
            }

            return array_map(
                static fn (string $cell) => trim($cell, " \t\n\r\0\x0B\""),
                explode($delimiter, $line)
            );
        };

        $headers = array_map(fn (string $h) => mb_strtolower($h), $splitLine($lines[0]));
        $findIdx = function (array $keys) use ($headers): int {
            foreach ($headers as $i => $header) {
                foreach ($keys as $key) {
                    if (str_contains((string) $header, $key)) {
                        return (int) $i;
                    }
                }
            }

            return -1;
        };

        $dateIdx = $findIdx(self::DATE_KEYS);
        $descIdx = $findIdx(self::DESC_KEYS);
        $amountIdx = $findIdx(self::AMOUNT_KEYS);
        $debitIdx = $findIdx(self::DEBIT_KEYS);
        $creditIdx = $findIdx(self::CREDIT_KEYS);

        $rows = [];
        $seen = $existingFingerprints;

        for ($i = 1; $i < count($lines); $i++) {
            $cells = $splitLine($lines[$i]);
            if ($cells === []) {
                continue;
            }

            $rawDate = $dateIdx >= 0 ? ($cells[$dateIdx] ?? '') : '';
            $date = self::parseDate((string) $rawDate);
            $name = $descIdx >= 0
                ? (string) ($cells[$descIdx] ?? '')
                : (string) (collect($cells)->first(fn ($c) => mb_strlen(trim((string) $c)) > 3) ?? 'Unknown');

            $amount = NAN;
            if ($amountIdx >= 0) {
                $amount = self::parseAmount((string) ($cells[$amountIdx] ?? ''));
            } elseif ($debitIdx >= 0 || $creditIdx >= 0) {
                $debit = $debitIdx >= 0 ? self::parseAmount((string) ($cells[$debitIdx] ?? '')) : 0.0;
                $credit = $creditIdx >= 0 ? self::parseAmount((string) ($cells[$creditIdx] ?? '')) : 0.0;
                if (is_finite($credit) && $credit > 0) {
                    $amount = $credit;
                } elseif (is_finite($debit) && $debit > 0) {
                    $amount = -abs($debit);
                }
            }

            if ($date === '' || ! is_finite($amount)) {
                continue;
            }

            $type = $amount > 0 ? 'income' : 'expense';
            $detected = CategoryDetector::detect($name);
            $categoryId = $detected ?? ($type === 'income' ? 'income' : 'other');
            $confidence = $detected ? 95 : 55;

            $print = self::fingerprint($date, $amount, $name);
            $duplicate = isset($seen[$print]);
            $seen[$print] = true;

            $rows[] = [
                'id' => 'imp-'.uniqid('', true).'-'.$i,
                'rawDate' => $rawDate,
                'date' => $date,
                'name' => mb_substr($name, 0, 80),
                'amount' => $amount,
                'type' => $type,
                'categoryId' => $categoryId,
                'confidence' => $confidence,
                'duplicate' => $duplicate,
                'selected' => ! $duplicate,
            ];
        }

        return $rows;
    }

    private static function detectDelimiter(string $sample): string
    {
        $counts = [',' => 0, ';' => 0, "\t" => 0];
        $len = mb_strlen($sample);
        for ($i = 0; $i < $len; $i++) {
            $ch = $sample[$i];
            if (isset($counts[$ch])) {
                $counts[$ch]++;
            }
        }
        arsort($counts);

        return array_key_first($counts) ?: ',';
    }

    /** @return list<string> */
    private static function splitCsvLine(string $line): array
    {
        $out = [];
        $current = '';
        $inQuote = false;
        $len = strlen($line);
        for ($i = 0; $i < $len; $i++) {
            $ch = $line[$i];
            if ($ch === '"') {
                if ($inQuote && $i + 1 < $len && $line[$i + 1] === '"') {
                    $current .= '"';
                    $i++;
                } else {
                    $inQuote = ! $inQuote;
                }
            } elseif (($ch === ',' || $ch === ';' || $ch === "\t") && ! $inQuote) {
                $out[] = trim($current);
                $current = '';
            } else {
                $current .= $ch;
            }
        }
        $out[] = trim($current);

        return $out;
    }

    private static function parseAmount(string $value): float
    {
        if ($value === '') {
            return NAN;
        }
        $cleaned = preg_replace('/[€$£\s]/u', '', $value) ?? '';
        $cleaned = preg_replace('/\.(?=\d{3}(?:[,.]|$))/', '', $cleaned) ?? '';
        $cleaned = str_replace(',', '.', $cleaned);
        $num = (float) $cleaned;

        return is_finite($num) ? $num : NAN;
    }

    private static function parseDate(string $value): string
    {
        $trimmed = trim($value);
        if ($trimmed === '') {
            return '';
        }
        if (preg_match('/^\d{4}-\d{2}-\d{2}/', $trimmed)) {
            return substr($trimmed, 0, 10);
        }
        if (preg_match('/^(\d{1,2})[\/.\-](\d{1,2})[\/.\-](\d{2,4})/', $trimmed, $m)) {
            $d = $m[1];
            $mo = $m[2];
            $y = $m[3];
            if (strlen($y) === 2) {
                $y = '20'.$y;
            }
            $y = str_pad($y, 4, '0', STR_PAD_LEFT);

            return $y.'-'.str_pad($mo, 2, '0', STR_PAD_LEFT).'-'.str_pad($d, 2, '0', STR_PAD_LEFT);
        }
        $ts = strtotime($trimmed);

        return $ts ? date('Y-m-d', $ts) : '';
    }
}
