<?php

declare(strict_types=1);

final class Asm3Analyzer
{
    private const DOMAINS = [
        'animal' => ['Animaux', 'animals', 'ready'],
        'owner' => ['Contacts et structures', 'directory_contacts', 'planned'],
        'adoption' => ['Mouvements, FA et adoptions', 'animal_placements / adoptions', 'planned'],
        'animalvaccination' => ['Vaccinations', 'vaccinations', 'planned'],
        'medical' => ['Traitements et actes médicaux', 'medical_notes / treatments', 'planned'],
        'animalmedical' => ['Traitements et actes médicaux', 'medical_notes / treatments', 'planned'],
        'media' => ['Médias', 'animal_photos / medical_photos', 'planned'],
        'animallitter' => ['Portées', 'litters', 'planned'],
        'animalcontrol' => ['Incidents et contrôles', 'non pris en charge', 'review'],
        'animaltest' => ['Tests médicaux', 'medical_notes', 'planned'],
    ];

    public static function analyze(string $path): array
    {
        if (!is_file($path) || !is_readable($path)) {
            throw new RuntimeException(t('asm3.unreadable'));
        }
        if (self::isCopyDump($path)) {
            return self::analyzeCopy($path);
        }
        $handle = fopen($path, 'rb');
        if (!$handle) {
            throw new RuntimeException(t('asm3.open_failed'));
        }
        $counts = [];
        $columns = [];
        $animalStats = [
            'active' => 0,
            'archived' => 0,
            'adoptable' => 0,
            'not_adoptable' => 0,
            'deceased' => 0,
            'microchip' => 0,
            'tattoo' => 0,
            'neutered' => 0,
            'missing_name' => 0,
            'missing_birth_date' => 0,
            'species' => [],
        ];
        $statement = '';
        $quoted = false;
        $bytes = 0;
        try {
            while (($chunk = fgets($handle)) !== false) {
                $bytes += strlen($chunk);
                $statement .= $chunk;
                $length = strlen($statement);
                $quoted = false;
                for ($i = 0; $i < $length; $i++) {
                    if ($statement[$i] !== "'") {
                        continue;
                    }
                    if ($quoted && $i + 1 < $length && $statement[$i + 1] === "'") {
                        $i++;
                        continue;
                    }
                    $quoted = !$quoted;
                }
                if ($quoted || !preg_match('/;\s*$/s', $statement)) {
                    continue;
                }
                self::consume($statement, $counts, $columns, $animalStats);
                $statement = '';
            }
            if (trim($statement) !== '') {
                self::consume($statement, $counts, $columns, $animalStats);
            }
        } finally {
            fclose($handle);
        }
        arsort($counts);
        $domains = [];
        foreach (self::DOMAINS as $table => [$label, $target, $status]) {
            $domains[] = [
                'table' => $table,
                'label' => $label,
                'target' => $target,
                'status' => $status,
                'rows' => $counts[$table] ?? 0,
                'present' => isset($counts[$table]),
            ];
        }
        $warnings = [];
        if (!isset($counts['animal'])) {
            $warnings[] = t('asm3.animal_table_missing');
        }
        if (($counts['animal'] ?? 0) > 0 && $animalStats['missing_name'] > 0) {
            $warnings[] = t('asm3.animals_without_name', ['count' => $animalStats['missing_name']]);
        }
        if (($counts['animal'] ?? 0) > 0 && $animalStats['missing_birth_date'] > 0) {
            $warnings[] = t('asm3.animals_without_birth', ['count' => $animalStats['missing_birth_date']]);
        }
        $unsupported = [];
        foreach ($counts as $table => $count) {
            if (!isset(self::DOMAINS[$table]) && $count > 0) {
                $unsupported[$table] = $count;
            }
        }
        return [
            'format' => 'ASM3 SQL INSERT',
            'source' => basename($path),
            'size_bytes' => filesize($path),
            'read_bytes' => $bytes,
            'generated_at' => date(DATE_ATOM),
            'tables' => $counts,
            'columns' => $columns,
            'domains' => $domains,
            'animals' => $animalStats,
            'warnings' => $warnings,
            'unmapped_tables' => $unsupported,
            'read_only' => true,
        ];
    }

    public static function rows(string $path, array $wantedTables): array
    {
        if (self::isCopyDump($path)) {
            return self::copyRows($path, $wantedTables);
        }
        $wanted = array_fill_keys(array_map('strtolower', $wantedTables), true);
        $rows = [];
        $columns = [];
        $handle = fopen($path, 'rb');
        if (!$handle) {
            throw new RuntimeException(t('asm3.open_failed'));
        }
        $statement = '';
        try {
            while (($chunk = fgets($handle)) !== false) {
                $statement .= $chunk;
                $quoted = false;
                $length = strlen($statement);
                for ($i = 0; $i < $length; $i++) {
                    if ($statement[$i] !== "'") {
                        continue;
                    }
                    if ($quoted && $i + 1 < $length && $statement[$i + 1] === "'") {
                        $i++;
                        continue;
                    }
                    $quoted = !$quoted;
                }
                if ($quoted || !preg_match('/;\s*$/s', $statement)) {
                    continue;
                }
                if (
                    preg_match(
                        '/^\s*INSERT\s+INTO\s+[`"]?([a-zA-Z0-9_]+)[`"]?\s*\((.*?)\)\s*VALUES\s*\((.*)\)\s*;\s*$/is',
                        $statement,
                        $m,
                    )
                ) {
                    $table = strtolower($m[1]);
                    if (isset($wanted[$table])) {
                        $cols = $columns[$table] ??= array_map(
                            static fn($v) => strtoupper(trim($v, " \t\r\n`\"")),
                            explode(',', $m[2]),
                        );
                        $values = self::values($m[3]);
                        $row = [];
                        foreach ($cols as $i => $column) {
                            $row[$column] = $values[$i] ?? null;
                        }
                        $rows[$table][] = $row;
                    }
                }
                $statement = '';
            }
        } finally {
            fclose($handle);
        }
        return $rows;
    }

    private static function consume(string $sql, array &$counts, array &$columns, array &$animalStats): void
    {
        if (
            !preg_match(
                '/^\s*INSERT\s+INTO\s+[`"]?([a-zA-Z0-9_]+)[`"]?\s*\((.*?)\)\s*VALUES\s*\((.*)\)\s*;\s*$/is',
                $sql,
                $m,
            )
        ) {
            return;
        }
        $table = strtolower($m[1]);
        $counts[$table] = ($counts[$table] ?? 0) + 1;
        if (!isset($columns[$table])) {
            $columns[$table] = array_map(static fn($v) => strtoupper(trim($v, " \t\r\n`\"")), explode(',', $m[2]));
        }
        if ($table !== 'animal') {
            return;
        }
        $values = self::values($m[3]);
        $row = [];
        foreach ($columns[$table] as $i => $column) {
            $row[$column] = $values[$i] ?? null;
        }
        $truthy = static fn($v): bool => (string) $v === '1';
        $blank = static fn($v): bool => $v === null || trim((string) $v) === '';
        $animalStats[$truthy($row['ARCHIVED'] ?? 0) ? 'archived' : 'active']++;
        $animalStats[$truthy($row['ADOPTABLE'] ?? 0) ? 'adoptable' : 'not_adoptable']++;
        if (!$blank($row['DECEASEDDATE'] ?? null) || $truthy($row['PUTTOSLEEP'] ?? 0)) {
            $animalStats['deceased']++;
        }
        if (!$blank($row['IDENTICHIPNUMBER'] ?? null) || $truthy($row['IDENTICHIPPED'] ?? 0)) {
            $animalStats['microchip']++;
        }
        if (!$blank($row['TATTOONUMBER'] ?? null) || $truthy($row['TATTOO'] ?? 0)) {
            $animalStats['tattoo']++;
        }
        if ($truthy($row['NEUTERED'] ?? 0)) {
            $animalStats['neutered']++;
        }
        if ($blank($row['ANIMALNAME'] ?? null)) {
            $animalStats['missing_name']++;
        }
        if ($blank($row['DATEOFBIRTH'] ?? null)) {
            $animalStats['missing_birth_date']++;
        }
        $species = (string) ($row['SPECIESID'] ?? 'unknown');
        $animalStats['species'][$species] = ($animalStats['species'][$species] ?? 0) + 1;
    }

    private static function isCopyDump(string $path): bool
    {
        $handle = @fopen($path, 'rb');
        if (!$handle) {
            return false;
        }
        $sample = (string) fread($handle, 262144);
        fclose($handle);
        return preg_match('/^COPY\s+(?:public\.)?[a-zA-Z0-9_]+\s*\(/mi', $sample) === 1;
    }

    private static function analyzeCopy(string $path): array
    {
        $counts = [];
        $columns = [];
        $animalStats = [
            'active' => 0,
            'archived' => 0,
            'adoptable' => 0,
            'not_adoptable' => 0,
            'deceased' => 0,
            'microchip' => 0,
            'tattoo' => 0,
            'neutered' => 0,
            'missing_name' => 0,
            'missing_birth_date' => 0,
            'species' => [],
        ];
        $handle = fopen($path, 'rb');
        if (!$handle) {
            throw new RuntimeException(t('asm3.open_failed'));
        }
        $table = null;
        $tableColumns = [];
        $bytes = 0;
        try {
            while (($line = fgets($handle)) !== false) {
                $bytes += strlen($line);
                $line = rtrim($line, "\r\n");
                if ($table === null) {
                    if (
                        preg_match(
                            '/^COPY\s+(?:public\.)?[`"]?([a-zA-Z0-9_]+)[`"]?\s*\((.*?)\)\s+FROM\s+stdin;$/i',
                            $line,
                            $m,
                        )
                    ) {
                        $table = strtolower($m[1]);
                        $tableColumns = array_map(static fn($v) => strtoupper(trim($v, " \t`\"")), explode(',', $m[2]));
                        $columns[$table] = $tableColumns;
                        $counts[$table] ??= 0;
                    }
                    continue;
                }
                if ($line === '\\.') {
                    $table = null;
                    $tableColumns = [];
                    continue;
                }
                $counts[$table]++;
                if ($table === 'animal') {
                    self::accumulateAnimalStats(self::copyRow($line, $tableColumns), $animalStats);
                }
            }
        } finally {
            fclose($handle);
        }
        arsort($counts);
        $domains = [];
        foreach (self::DOMAINS as $name => [$label, $target, $status]) {
            $domains[] = [
                'table' => $name,
                'label' => $label,
                'target' => $target,
                'status' => $status,
                'rows' => $counts[$name] ?? 0,
                'present' => isset($counts[$name]),
            ];
        }
        $warnings = [];
        if (!isset($counts['animal'])) {
            $warnings[] = t('asm3.animal_table_missing');
        }
        if (($counts['animal'] ?? 0) > 0 && $animalStats['missing_name'] > 0) {
            $warnings[] = t('asm3.animals_without_name', ['count' => $animalStats['missing_name']]);
        }
        if (($counts['animal'] ?? 0) > 0 && $animalStats['missing_birth_date'] > 0) {
            $warnings[] = t('asm3.animals_without_birth', ['count' => $animalStats['missing_birth_date']]);
        }
        $unsupported = [];
        foreach ($counts as $name => $count) {
            if (!isset(self::DOMAINS[$name]) && $count > 0) {
                $unsupported[$name] = $count;
            }
        }
        return [
            'format' => 'ASM3 PostgreSQL COPY',
            'source' => basename($path),
            'size_bytes' => filesize($path),
            'read_bytes' => $bytes,
            'generated_at' => date(DATE_ATOM),
            'tables' => $counts,
            'columns' => $columns,
            'domains' => $domains,
            'animals' => $animalStats,
            'warnings' => $warnings,
            'unmapped_tables' => $unsupported,
            'read_only' => true,
        ];
    }

    private static function copyRows(string $path, array $wantedTables): array
    {
        $wanted = array_fill_keys(array_map('strtolower', $wantedTables), true);
        $rows = [];
        $handle = fopen($path, 'rb');
        if (!$handle) {
            throw new RuntimeException(t('asm3.open_failed'));
        }
        $table = null;
        $columns = [];
        $capture = false;
        try {
            while (($line = fgets($handle)) !== false) {
                $line = rtrim($line, "\r\n");
                if ($table === null) {
                    if (
                        preg_match(
                            '/^COPY\s+(?:public\.)?[`"]?([a-zA-Z0-9_]+)[`"]?\s*\((.*?)\)\s+FROM\s+stdin;$/i',
                            $line,
                            $m,
                        )
                    ) {
                        $table = strtolower($m[1]);
                        $capture = isset($wanted[$table]);
                        $columns = $capture
                            ? array_map(static fn($v) => strtoupper(trim($v, " \t`\"")), explode(',', $m[2]))
                            : [];
                    }
                    continue;
                }
                if ($line === '\\.') {
                    $table = null;
                    $columns = [];
                    $capture = false;
                    continue;
                }
                if ($capture) {
                    $rows[$table][] = self::copyRow($line, $columns);
                }
            }
        } finally {
            fclose($handle);
        }
        return $rows;
    }

    private static function copyRow(string $line, array $columns): array
    {
        $values = explode("\t", $line);
        $row = [];
        foreach ($columns as $i => $column) {
            $row[$column] = self::copyScalar($values[$i] ?? '\\N');
        }
        return $row;
    }

    private static function copyScalar(string $value): ?string
    {
        if ($value === '\\N') {
            return null;
        }
        return preg_replace_callback(
            '/\\\\([0-7]{1,3}|.)/s',
            static function (array $m): string {
                $escape = $m[1];
                if (ctype_digit($escape)) {
                    return chr(octdec($escape));
                }
                return match ($escape) {
                    'b' => "\x08",
                    'f' => "\x0c",
                    'n' => "\n",
                    'r' => "\r",
                    't' => "\t",
                    'v' => "\x0b",
                    '\\' => '\\',
                    default => $escape,
                };
            },
            $value,
        ) ?? $value;
    }

    private static function accumulateAnimalStats(array $row, array &$animalStats): void
    {
        $truthy = static fn($v): bool => (string) $v === '1';
        $blank = static fn($v): bool => $v === null || trim((string) $v) === '';
        $animalStats[$truthy($row['ARCHIVED'] ?? 0) ? 'archived' : 'active']++;
        $animalStats[$truthy($row['ADOPTABLE'] ?? 0) ? 'adoptable' : 'not_adoptable']++;
        if (!$blank($row['DECEASEDDATE'] ?? null) || $truthy($row['PUTTOSLEEP'] ?? 0)) {
            $animalStats['deceased']++;
        }
        if (!$blank($row['IDENTICHIPNUMBER'] ?? null) || $truthy($row['IDENTICHIPPED'] ?? 0)) {
            $animalStats['microchip']++;
        }
        if (!$blank($row['TATTOONUMBER'] ?? null) || $truthy($row['TATTOO'] ?? 0)) {
            $animalStats['tattoo']++;
        }
        if ($truthy($row['NEUTERED'] ?? 0)) {
            $animalStats['neutered']++;
        }
        if ($blank($row['ANIMALNAME'] ?? null)) {
            $animalStats['missing_name']++;
        }
        if ($blank($row['DATEOFBIRTH'] ?? null)) {
            $animalStats['missing_birth_date']++;
        }
        $species = (string) ($row['SPECIESID'] ?? 'unknown');
        $animalStats['species'][$species] = ($animalStats['species'][$species] ?? 0) + 1;
    }

    private static function values(string $input): array
    {
        $values = [];
        $value = '';
        $quoted = false;
        $length = strlen($input);
        for ($i = 0; $i < $length; $i++) {
            $char = $input[$i];
            if ($char === "'") {
                if ($quoted && $i + 1 < $length && $input[$i + 1] === "'") {
                    $value .= "'";
                    $i++;
                    continue;
                }
                $quoted = !$quoted;
                continue;
            }
            if ($char === ',' && !$quoted) {
                $values[] = self::scalar($value);
                $value = '';
                continue;
            }
            $value .= $char;
        }
        $values[] = self::scalar($value);
        return $values;
    }

    private static function scalar(string $value): mixed
    {
        $value = trim($value);
        if (strcasecmp($value, 'null') === 0) {
            return null;
        }
        return $value;
    }
}
