<?php

declare(strict_types=1);

final class StatisticsService
{
    public static function costs(?int $year, ?array $ids = null): array
    {
        $db = DB::pdo();
        $scopeIds = self::costScopeIds($year, $ids);
        $empty = [
            'total_cents' => 0,
            'animals' => 0,
            'scope_animals' => count($scopeIds),
            'average_all_cents' => 0,
            'average_expense_cents' => 0,
            'median_expense_cents' => 0,
            'by_clinic' => [],
            'by_category' => [],
        ];
        if (!$scopeIds) {
            return $empty;
        }
        $where = ['a.deleted_at IS NULL', 'ae.animal_id IN(' . implode(',', array_map('intval', $scopeIds)) . ')'];
        $params = [];
        if ($year !== null) {
            $where[] = 'date(ae.occurred_on) BETWEEN :start AND :end';
            $params = [':start' => sprintf('%04d-01-01', $year), ':end' => sprintf('%04d-12-31', $year)];
        }
        $base =
            ' FROM animal_expenses ae JOIN animals a ON a.id=ae.animal_id LEFT JOIN directory_contacts dc ON dc.id=ae.clinic_contact_id LEFT JOIN clinic_tariffs ct ON ct.id=ae.tariff_id WHERE ' .
            implode(' AND ', $where);
        $map = static function (string $sql) use ($db, $params): array {
            $s = $db->prepare($sql);
            $s->execute($params);
            return $s->fetchAll(PDO::FETCH_ASSOC);
        };
        $perAnimal = $map(
            'SELECT ae.animal_id,SUM(ae.total_cents) total_cents' .
                $base .
                ' GROUP BY ae.animal_id ORDER BY total_cents',
        );
        $totals = array_map(static fn($row) => (int) $row['total_cents'], $perAnimal);
        $total = array_sum($totals);
        $animals = count($totals);
        return [
            'total_cents' => $total,
            'animals' => $animals,
            'scope_animals' => count($scopeIds),
            'average_all_cents' => count($scopeIds) ? (int) round($total / count($scopeIds)) : 0,
            'average_expense_cents' => $animals ? (int) round($total / $animals) : 0,
            'median_expense_cents' => self::median($totals),
            'by_clinic' => $map(
                "SELECT dc.id contact_id,COALESCE(dc.name,'—') label,COUNT(DISTINCT ae.animal_id) animals,SUM(ae.total_cents) total_cents,ROUND(SUM(ae.total_cents)*1.0/COUNT(DISTINCT ae.animal_id)) average_cents" .
                    $base .
                    ' GROUP BY dc.id,dc.name ORDER BY total_cents DESC',
            ),
            'by_category' => $map(
                "SELECT COALESCE(ct.category,'other') label,COUNT(DISTINCT ae.animal_id) animals,SUM(ae.total_cents) total_cents,ROUND(SUM(ae.total_cents)*1.0/COUNT(DISTINCT ae.animal_id)) average_cents" .
                    $base .
                    ' GROUP BY ct.category ORDER BY total_cents DESC',
            ),
        ];
    }

    public static function costsByMunicipality(?int $year): array
    {
        $db = DB::pdo();
        $scopeIds = self::costScopeIds($year, null);
        if (!$scopeIds) {
            return [];
        }
        $animals = $db
            ->query('SELECT id,rescue_location_name,rescue_address FROM animals WHERE deleted_at IS NULL')
            ->fetchAll(PDO::FETCH_ASSOC);
        $cities = [];
        $animalCity = [];
        foreach ($animals as $row) {
            $id = (int) $row['id'];
            if (!in_array($id, $scopeIds, true)) {
                continue;
            }
            $city = GrantService::municipality((string) ($row['rescue_location_name'] ?: $row['rescue_address']));
            if ($city === 'Non renseignée') {
                continue;
            }
            $animalCity[$id] = $city;
            $cities[$city]['animal_ids'][$id] = true;
        }
        if (!$animalCity) {
            return [];
        }
        $params = [];
        $dateSql = '';
        if ($year !== null) {
            $dateSql = ' AND date(occurred_on) BETWEEN :start AND :end';
            $params = [':start' => sprintf('%04d-01-01', $year), ':end' => sprintf('%04d-12-31', $year)];
        }
        $s = $db->prepare(
            'SELECT animal_id,SUM(total_cents) total_cents FROM animal_expenses WHERE animal_id IN(' .
                implode(',', array_keys($animalCity)) .
                ')' .
                $dateSql .
                ' GROUP BY animal_id',
        );
        $s->execute($params);
        foreach ($s->fetchAll(PDO::FETCH_ASSOC) as $row) {
            $id = (int) $row['animal_id'];
            $city = $animalCity[$id];
            $cities[$city]['expense_totals'][$id] = (int) $row['total_cents'];
        }
        $out = [];
        foreach ($cities as $city => $data) {
            $totals = array_values($data['expense_totals'] ?? []);
            $total = array_sum($totals);
            $scope = count($data['animal_ids']);
            $with = count($totals);
            $out[] = [
                'municipality' => $city,
                'animals' => $scope,
                'animals_with_expense' => $with,
                'total_cents' => $total,
                'average_all_cents' => $scope ? (int) round($total / $scope) : 0,
                'average_expense_cents' => $with ? (int) round($total / $with) : 0,
                'median_expense_cents' => self::median($totals),
            ];
        }
        usort(
            $out,
            static fn($a, $b) => $b['total_cents'] <=> $a['total_cents'] ?:
            strnatcasecmp($a['municipality'], $b['municipality']),
        );
        return $out;
    }

    private static function costScopeIds(?int $year, ?array $ids): array
    {
        $db = DB::pdo();
        $limit = $ids === null ? '' : self::idSql($ids, 'a.id');
        if ($year === null) {
            $sql = 'SELECT a.id FROM animals a WHERE a.deleted_at IS NULL' . $limit;
        } else {
            $start = sprintf('%04d-01-01', $year);
            $end = sprintf('%04d-12-31', $year);
            $sql =
                'SELECT a.id FROM animals a WHERE a.deleted_at IS NULL' .
                $limit .
                " AND (date(a.intake_date) BETWEEN :start AND :end OR EXISTS(SELECT 1 FROM animal_movements m WHERE m.animal_id=a.id AND m.kind='entry' AND date(m.created_at) BETWEEN :start AND :end) OR EXISTS(SELECT 1 FROM animal_expenses ae WHERE ae.animal_id=a.id AND date(ae.occurred_on) BETWEEN :start AND :end))";
        }
        $s = $db->prepare($sql);
        $s->execute($year === null ? [] : [':start' => $start, ':end' => $end]);
        return array_map('intval', $s->fetchAll(PDO::FETCH_COLUMN));
    }

    private static function median(array $values): int
    {
        if (!$values) {
            return 0;
        }
        sort($values, SORT_NUMERIC);
        $count = count($values);
        $middle = intdiv($count, 2);
        return $count % 2 ? (int) $values[$middle] : (int) round(($values[$middle - 1] + $values[$middle]) / 2);
    }
    private static function idSql(?array $ids, string $column = 'animal_id'): string
    {
        if ($ids === null) {
            return '';
        }
        if (!$ids) {
            return ' AND 1=0';
        }
        return ' AND ' . $column . ' IN (' . implode(',', array_map('intval', $ids)) . ')';
    }

    public static function municipalities(): array
    {
        $rows = DB::pdo()
            ->query('SELECT id,rescue_location_name,rescue_address FROM animals WHERE deleted_at IS NULL')
            ->fetchAll(PDO::FETCH_ASSOC);
        $items = [];
        foreach ($rows as $row) {
            $city = GrantService::municipality((string) ($row['rescue_location_name'] ?: $row['rescue_address']));
            if ($city === 'Non renseignée') {
                continue;
            }
            $items[$city] = ($items[$city] ?? 0) + 1;
        }
        uksort($items, static fn($a, $b) => strnatcasecmp($a, $b));
        return $items;
    }

    public static function animalIdsForMunicipality(string $municipality): array
    {
        if ($municipality === '') {
            return [];
        }
        $rows = DB::pdo()
            ->query('SELECT id,rescue_location_name,rescue_address FROM animals WHERE deleted_at IS NULL')
            ->fetchAll(PDO::FETCH_ASSOC);
        $ids = [];
        foreach ($rows as $row) {
            if (
                GrantService::municipality((string) ($row['rescue_location_name'] ?: $row['rescue_address'])) ===
                $municipality
            ) {
                $ids[] = (int) $row['id'];
            }
        }
        return $ids;
    }

    public static function municipalityCoverage(): array
    {
        $row =
            DB::pdo()
                ->query(
                    "SELECT COUNT(*) total,SUM(CASE WHEN TRIM(COALESCE(rescue_location_name,''))<>'' OR TRIM(COALESCE(rescue_address,''))<>'' THEN 1 ELSE 0 END) known,SUM(CASE WHEN (TRIM(COALESCE(rescue_location_name,''))<>'' OR TRIM(COALESCE(rescue_address,''))<>'') AND COALESCE(rescue_location_confidence,'')<>'inferred_family' THEN 1 ELSE 0 END) documented,SUM(CASE WHEN rescue_location_confidence='inferred_family' THEN 1 ELSE 0 END) inferred,SUM(CASE WHEN intake_type='born_in_care' OR intake_reason='birth' THEN 1 ELSE 0 END) born_in_care,SUM(CASE WHEN TRIM(COALESCE(rescue_location_name,''))='' AND TRIM(COALESCE(rescue_address,''))='' AND intake_type<>'born_in_care' AND intake_reason<>'birth' THEN 1 ELSE 0 END) unknown FROM animals WHERE deleted_at IS NULL",
                )
                ->fetch(PDO::FETCH_ASSOC) ?:
            [];
        return array_map('intval', $row);
    }

    public static function annual(int $year, ?array $ids = null): array
    {
        if ($ids === null) {
            $stats = GrantService::statistics($year);
        } else {
            $db = DB::pdo();
            $p = [':start' => sprintf('%04d-01-01', $year), ':end' => sprintf('%04d-12-31', $year)];
            $in = self::idSql($ids);
            $scalar = static function (string $sql) use ($db, $p): float {
                $s = $db->prepare($sql);
                $s->execute($p);
                return (float) ($s->fetchColumn() ?: 0);
            };
            $stats = [];
            $stats['entries'] = $scalar(
                "SELECT COUNT(*) FROM (SELECT m.animal_id FROM animal_movements m JOIN animals a ON a.id=m.animal_id WHERE a.deleted_at IS NULL AND m.kind='entry' AND date(m.created_at) BETWEEN :start AND :end" .
                    self::idSql($ids, 'm.animal_id') .
                    ' UNION SELECT a.id FROM animals a WHERE a.deleted_at IS NULL AND date(a.intake_date) BETWEEN :start AND :end' .
                    self::idSql($ids, 'a.id') .
                    ')',
            );
            $stats['exits'] = $scalar(
                'SELECT (SELECT COUNT(*) FROM adoptions ad JOIN animals a ON a.id=ad.animal_id WHERE a.deleted_at IS NULL AND date(ad.adoption_date) BETWEEN :start AND :end' .
                    self::idSql($ids, 'ad.animal_id') .
                    ")+(SELECT COUNT(*) FROM animal_deaths d JOIN animals a ON a.id=d.animal_id WHERE a.deleted_at IS NULL AND date(COALESCE(d.deceased_date,printf('%04d-01-01',d.death_year))) BETWEEN :start AND :end AND NOT EXISTS(SELECT 1 FROM adoptions ad WHERE ad.animal_id=d.animal_id)" .
                    self::idSql($ids, 'd.animal_id') .
                    ')',
            );
            $stats['adoptions'] = $scalar(
                'SELECT COUNT(*) FROM adoptions WHERE date(adoption_date) BETWEEN :start AND :end' . $in,
            );
            $stats['deaths'] = $scalar(
                "SELECT COUNT(*) FROM animal_deaths WHERE date(COALESCE(deceased_date,printf('%04d-01-01',death_year))) BETWEEN :start AND :end" .
                    $in,
            );
            $stats['births'] = $scalar(
                "SELECT COUNT(*) FROM litter_kittens lk JOIN litters l ON l.id=lk.litter_id JOIN animals a ON a.id=lk.animal_id WHERE a.deleted_at IS NULL AND date(COALESCE(NULLIF(l.birth_date,''),NULLIF(a.birth_date,''))) BETWEEN :start AND :end" .
                    self::idSql($ids, 'lk.animal_id'),
            );
            $stats['vaccinations'] = $scalar(
                'SELECT COUNT(*) FROM vaccinations WHERE date(done_date) BETWEEN :start AND :end' . $in,
            );
            $stats['treatments'] = $scalar(
                'SELECT COUNT(*) FROM treatments WHERE date(start_date) BETWEEN :start AND :end' . $in,
            );
            $stats['avg'] = $scalar(
                "SELECT AVG(julianday(adoption_date)-julianday(COALESCE(NULLIF(a.intake_date,''),date(a.created_at)))) FROM adoptions ad JOIN animals a ON a.id=ad.animal_id WHERE date(adoption_date) BETWEEN :start AND :end AND date(adoption_date)>=date(COALESCE(NULLIF(a.intake_date,''),date(a.created_at)))" .
                    self::idSql($ids, 'a.id'),
            );
            $stats['current'] =
                (float) ($db
                    ->query(
                        "SELECT COUNT(*) FROM animals WHERE deleted_at IS NULL AND archived_at IS NULL AND status NOT IN ('adopte','decede')" .
                            self::idSql($ids, 'id'),
                    )
                    ->fetchColumn() ?:
                0);
        }
        $db = DB::pdo();
        $range = [':start' => sprintf('%04d-01-01', $year), ':end' => sprintf('%04d-12-31', $year)];
        $filter = self::idSql($ids, 'a.id');
        $s = $db->prepare(
            "SELECT COUNT(*) FROM (SELECT m.animal_id FROM animal_movements m JOIN animals a ON a.id=m.animal_id WHERE a.deleted_at IS NULL AND m.kind='entry' AND date(m.created_at) BETWEEN :start AND :end" .
                $filter .
                ' UNION SELECT a.id FROM animals a WHERE a.deleted_at IS NULL AND date(a.intake_date) BETWEEN :start AND :end' .
                $filter .
                " UNION SELECT a.id FROM animals a JOIN animal_deaths d ON d.animal_id=a.id WHERE a.deleted_at IS NULL AND NULLIF(TRIM(a.intake_date),'') IS NULL AND NOT EXISTS(SELECT 1 FROM animal_movements em WHERE em.animal_id=a.id AND em.kind='entry') AND date(COALESCE(d.deceased_date,printf('%04d-01-01',d.death_year))) BETWEEN :start AND :end" .
                $filter .
                ')',
        );
        $s->execute($range);
        $stats['handled'] = (int) $s->fetchColumn();
        $s = $db->prepare(
            'SELECT COUNT(*) FROM medical_notes mn JOIN animals a ON a.id=mn.animal_id WHERE date(mn.noted_at) BETWEEN :start AND :end' .
                $filter,
        );
        $s->execute($range);
        $stats['medical_notes'] = (int) $s->fetchColumn();
        return $stats;
    }

    public static function comparison(int $year, ?array $ids = null): array
    {
        $current = self::annual($year, $ids);
        $previous = self::annual($year - 1, $ids);
        $out = [];
        foreach (
            [
                'handled',
                'entries',
                'exits',
                'adoptions',
                'deaths',
                'births',
                'vaccinations',
                'treatments',
                'medical_notes',
                'avg',
            ]
            as $key
        ) {
            $a = (float) $current[$key];
            $b = (float) $previous[$key];
            $out[$key] = [
                'current' => $a,
                'previous' => $b,
                'difference' => $a - $b,
                'percent' => $b != 0 ? (($a - $b) / $b) * 100 : null,
            ];
        }
        return $out;
    }

    public static function years(): array
    {
        $db = DB::pdo();
        $found = [];
        $queries = [
            'SELECT intake_date d FROM animals WHERE deleted_at IS NULL AND intake_date IS NOT NULL',
            "SELECT created_at d FROM animal_movements WHERE kind IN ('entry','exit')",
            'SELECT adoption_date d FROM adoptions',
            "SELECT COALESCE(deceased_date,printf('%04d-01-01',death_year)) d FROM animal_deaths WHERE deceased_date IS NOT NULL OR death_year IS NOT NULL",
            'SELECT birth_date d FROM litters WHERE birth_date IS NOT NULL',
            'SELECT done_date d FROM vaccinations',
            'SELECT start_date d FROM treatments',
            'SELECT noted_at d FROM medical_notes',
            'SELECT occurred_on d FROM animal_expenses',
        ];
        foreach ($queries as $sql) {
            foreach ($db->query($sql)->fetchAll(PDO::FETCH_COLUMN) as $date) {
                $y = (int) substr((string) $date, 0, 4);
                if ($y >= 2000 && $y <= 2200) {
                    $found[$y] = true;
                }
            }
        }
        if (!$found) {
            $found[(int) date('Y')] = true;
        }
        $first = min(array_keys($found));
        $last = max((int) date('Y'), max(array_keys($found)));
        return range($first, $last);
    }

    public static function timeline(?array $years = null, ?array $ids = null): array
    {
        $out = [];
        foreach ($years ?? self::years() as $year) {
            $out[(int) $year] = self::annual((int) $year, $ids);
        }
        return $out;
    }

    public static function all(?array $ids = null): array
    {
        $stats = array_fill_keys(
            ['entries', 'exits', 'adoptions', 'deaths', 'births', 'vaccinations', 'treatments', 'medical_notes'],
            0.0,
        );
        foreach (self::timeline(null, $ids) as $annual) {
            foreach (array_keys($stats) as $key) {
                $stats[$key] += (float) $annual[$key];
            }
        }
        $db = DB::pdo();
        $stats['deaths'] =
            (float) ($db
                ->query(
                    'SELECT COUNT(*) FROM animal_deaths d JOIN animals a ON a.id=d.animal_id WHERE a.deleted_at IS NULL' .
                        self::idSql($ids, 'd.animal_id'),
                )
                ->fetchColumn() ?:
            0);
        if ($ids === null) {
            $stats['handled'] =
                (float) ($db->query('SELECT COUNT(*) FROM animals WHERE deleted_at IS NULL')->fetchColumn() ?: 0);
            $stats['avg'] =
                (float) ($db
                    ->query(
                        "SELECT AVG(julianday(adoption_date)-julianday(COALESCE(NULLIF(a.intake_date,''),date(a.created_at)))) FROM adoptions ad JOIN animals a ON a.id=ad.animal_id WHERE adoption_date IS NOT NULL AND date(adoption_date)>=date(COALESCE(NULLIF(a.intake_date,''),date(a.created_at)))",
                    )
                    ->fetchColumn() ?:
                0);
        } else {
            $stats['handled'] = (float) count($ids);
            $stats['avg'] =
                (float) ($db
                    ->query(
                        "SELECT AVG(julianday(adoption_date)-julianday(COALESCE(NULLIF(a.intake_date,''),date(a.created_at)))) FROM adoptions ad JOIN animals a ON a.id=ad.animal_id WHERE adoption_date IS NOT NULL AND date(adoption_date)>=date(COALESCE(NULLIF(a.intake_date,''),date(a.created_at)))" .
                            self::idSql($ids, 'a.id'),
                    )
                    ->fetchColumn() ?:
                0);
        }
        $stats['entries'] = $stats['handled'];
        $stats['exits'] =
            (float) ($db
                ->query(
                    'SELECT (SELECT COUNT(*) FROM adoptions ad JOIN animals a ON a.id=ad.animal_id WHERE a.deleted_at IS NULL' .
                        self::idSql($ids, 'ad.animal_id') .
                        ')+(SELECT COUNT(*) FROM animal_deaths d JOIN animals a ON a.id=d.animal_id WHERE a.deleted_at IS NULL AND NOT EXISTS(SELECT 1 FROM adoptions ad WHERE ad.animal_id=d.animal_id)' .
                        self::idSql($ids, 'd.animal_id') .
                        ")+(SELECT COUNT(*) FROM animals a WHERE a.deleted_at IS NULL AND a.status='enfui' AND NOT EXISTS(SELECT 1 FROM adoptions ad WHERE ad.animal_id=a.id) AND NOT EXISTS(SELECT 1 FROM animal_deaths d WHERE d.animal_id=a.id)" .
                        self::idSql($ids, 'a.id') .
                        ')',
                )
                ->fetchColumn() ?:
            0);
        $stats['adoptions'] =
            (float) ($db
                ->query(
                    'SELECT COUNT(*) FROM adoptions ad JOIN animals a ON a.id=ad.animal_id WHERE a.deleted_at IS NULL' .
                        self::idSql($ids, 'ad.animal_id'),
                )
                ->fetchColumn() ?:
            0);
        $stats['births'] =
            (float) ($db
                ->query(
                    'SELECT COUNT(*) FROM litter_kittens lk JOIN animals a ON a.id=lk.animal_id WHERE a.deleted_at IS NULL' .
                        self::idSql($ids, 'lk.animal_id'),
                )
                ->fetchColumn() ?:
            0);
        $stats['vaccinations'] =
            (float) ($db
                ->query(
                    'SELECT COUNT(*) FROM vaccinations v JOIN animals a ON a.id=v.animal_id WHERE a.deleted_at IS NULL' .
                        self::idSql($ids, 'v.animal_id'),
                )
                ->fetchColumn() ?:
            0);
        $stats['medical_notes'] =
            (float) ($db
                ->query(
                    'SELECT COUNT(*) FROM medical_notes mn JOIN animals a ON a.id=mn.animal_id WHERE a.deleted_at IS NULL' .
                        self::idSql($ids, 'mn.animal_id'),
                )
                ->fetchColumn() ?:
            0);
        $stats['current'] = (float) (self::currentHealth($ids)['total'] ?? 0);
        return $stats;
    }

    public static function undatedHandled(?array $ids = null): int
    {
        return (int) (DB::pdo()
            ->query(
                "SELECT COUNT(*) FROM animals a WHERE a.deleted_at IS NULL AND (a.intake_date IS NULL OR TRIM(a.intake_date)='') AND NOT EXISTS(SELECT 1 FROM animal_movements m WHERE m.animal_id=a.id AND m.kind='entry')" .
                    self::idSql($ids, 'a.id'),
            )
            ->fetchColumn() ?:
        0);
    }

    public static function undatedDeaths(?array $ids = null): int
    {
        return (int) (DB::pdo()
            ->query(
                "SELECT COUNT(*) FROM animal_deaths d JOIN animals a ON a.id=d.animal_id WHERE a.deleted_at IS NULL AND (d.deceased_date IS NULL OR TRIM(d.deceased_date)='') AND d.death_year IS NULL" .
                    self::idSql($ids, 'd.animal_id'),
            )
            ->fetchColumn() ?:
        0);
    }

    public static function undatedBirths(?array $ids = null): int
    {
        return (int) (DB::pdo()
            ->query(
                "SELECT COUNT(*) FROM litter_kittens lk JOIN litters l ON l.id=lk.litter_id JOIN animals a ON a.id=lk.animal_id WHERE a.deleted_at IS NULL AND COALESCE(NULLIF(l.birth_date,''),NULLIF(a.birth_date,'')) IS NULL" .
                    self::idSql($ids, 'lk.animal_id'),
            )
            ->fetchColumn() ?:
        0);
    }

    public static function undatedExits(?array $ids = null): int
    {
        return (int) (DB::pdo()
            ->query(
                "SELECT (SELECT COUNT(*) FROM animal_deaths d JOIN animals a ON a.id=d.animal_id WHERE a.deleted_at IS NULL AND (d.deceased_date IS NULL OR TRIM(d.deceased_date)='') AND d.death_year IS NULL AND NOT EXISTS(SELECT 1 FROM adoptions ad WHERE ad.animal_id=d.animal_id)" .
                    self::idSql($ids, 'd.animal_id') .
                    ")+(SELECT COUNT(*) FROM animals a WHERE a.deleted_at IS NULL AND a.status='enfui' AND NOT EXISTS(SELECT 1 FROM adoptions ad WHERE ad.animal_id=a.id) AND NOT EXISTS(SELECT 1 FROM animal_deaths d WHERE d.animal_id=a.id)" .
                    self::idSql($ids, 'a.id') .
                    ')',
            )
            ->fetchColumn() ?:
        0);
    }

    public static function scopeCounts(?array $ids = null): array
    {
        $db = DB::pdo();
        return [
            'vaccinated_animals' => (int) $db
                ->query(
                    'SELECT COUNT(DISTINCT v.animal_id) FROM vaccinations v JOIN animals a ON a.id=v.animal_id WHERE a.deleted_at IS NULL' .
                        self::idSql($ids, 'v.animal_id'),
                )
                ->fetchColumn(),
            'medical_animals' => (int) $db
                ->query(
                    'SELECT COUNT(DISTINCT mn.animal_id) FROM medical_notes mn JOIN animals a ON a.id=mn.animal_id WHERE a.deleted_at IS NULL' .
                        self::idSql($ids, 'mn.animal_id'),
                )
                ->fetchColumn(),
        ];
    }

    private static function details(int $year, array $ids): array
    {
        $db = DB::pdo();
        $p = [':start' => sprintf('%04d-01-01', $year), ':end' => sprintf('%04d-12-31', $year)];
        $base =
            "WITH entry_sources AS (SELECT m.animal_id,date(m.created_at) entry_date FROM animal_movements m JOIN animals source_animal ON source_animal.id=m.animal_id WHERE source_animal.deleted_at IS NULL AND m.kind='entry' AND date(m.created_at) BETWEEN :start AND :end" .
            self::idSql($ids, 'm.animal_id') .
            ' UNION SELECT id animal_id,date(intake_date) entry_date FROM animals WHERE deleted_at IS NULL AND date(intake_date) BETWEEN :start AND :end' .
            self::idSql($ids, 'id') .
            '), entered AS (SELECT animal_id,MIN(entry_date) entry_date FROM entry_sources GROUP BY animal_id) ';
        $map = static function (string $sql) use ($db, $p): array {
            $s = $db->prepare($sql);
            $s->execute($p);
            $out = [];
            foreach ($s->fetchAll(PDO::FETCH_ASSOC) as $r) {
                $out[(string) $r['label']] = (int) $r['total'];
            }
            return $out;
        };
        $sex = $map(
            $base .
                "SELECT CASE a.sex WHEN 'F' THEN 'Femelles' WHEN 'M' THEN 'Mâles' ELSE 'Inconnu' END label,COUNT(*) total FROM entered e JOIN animals a ON a.id=e.animal_id GROUP BY a.sex ORDER BY total DESC",
        );
        $ages = $map(
            $base .
                "SELECT CASE WHEN a.birth_date IS NULL THEN 'Non renseigné' WHEN (julianday(e.entry_date)-julianday(a.birth_date))/365.25<1 THEN 'Moins d’un an' WHEN (julianday(e.entry_date)-julianday(a.birth_date))/365.25<3 THEN '1 à 2 ans' WHEN (julianday(e.entry_date)-julianday(a.birth_date))/365.25<8 THEN '3 à 7 ans' ELSE '8 ans et plus' END label,COUNT(*) total FROM entered e JOIN animals a ON a.id=e.animal_id GROUP BY label ORDER BY total DESC",
        );
        $deathCauses = $map(
            "SELECT cause_code label,COUNT(*) total FROM animal_deaths WHERE date(COALESCE(deceased_date,printf('%04d-01-01',death_year))) BETWEEN :start AND :end" .
                $in .
                ' GROUP BY cause_code ORDER BY total DESC',
        );
        $stays = $map(
            "SELECT CASE WHEN julianday(ad.adoption_date)-julianday(COALESCE(NULLIF(a.intake_date,''),date(a.created_at)))<30 THEN 'Moins d’un mois' WHEN julianday(ad.adoption_date)-julianday(COALESCE(NULLIF(a.intake_date,''),date(a.created_at)))<90 THEN '1 à 3 mois' WHEN julianday(ad.adoption_date)-julianday(COALESCE(NULLIF(a.intake_date,''),date(a.created_at)))<180 THEN '3 à 6 mois' WHEN julianday(ad.adoption_date)-julianday(COALESCE(NULLIF(a.intake_date,''),date(a.created_at)))<365 THEN '6 à 12 mois' ELSE 'Plus d’un an' END label,COUNT(*) total FROM adoptions ad JOIN animals a ON a.id=ad.animal_id WHERE date(ad.adoption_date) BETWEEN :start AND :end AND date(ad.adoption_date)>=date(COALESCE(NULLIF(a.intake_date,''),date(a.created_at)))" .
                self::idSql($ids, 'a.id') .
                ' GROUP BY label ORDER BY total DESC',
        );
        return compact('sex', 'ages', 'deathCauses', 'stays');
    }

    public static function distributions(?int $year, ?array $ids = null, ?string $municipality = null): array
    {
        $db = DB::pdo();
        $p = $year === null ? [] : [':start' => sprintf('%04d-01-01', $year), ':end' => sprintf('%04d-12-31', $year)];
        $rows = static function (string $sql, array $params = []) use ($db): array {
            $s = $db->prepare($sql);
            $s->execute($params);
            $out = [];
            foreach ($s->fetchAll(PDO::FETCH_ASSOC) as $r) {
                $out[(string) $r['label']] = (int) $r['total'];
            }
            return $out;
        };
        $movementDate = $year === null ? '' : ' AND date(m.created_at) BETWEEN :start AND :end';
        $animalDate = $year === null ? '' : ' AND date(intake_date) BETWEEN :start AND :end';
        $datedAnimal = $year === null ? '' : " AND NULLIF(TRIM(intake_date),'') IS NOT NULL";
        $entered =
            "WITH entry_sources AS (SELECT m.animal_id,date(m.created_at) entry_date FROM animal_movements m JOIN animals source_animal ON source_animal.id=m.animal_id WHERE source_animal.deleted_at IS NULL AND m.kind='entry'" .
            $movementDate .
            self::idSql($ids, 'm.animal_id') .
            ' UNION SELECT id animal_id,date(intake_date) entry_date FROM animals WHERE deleted_at IS NULL' .
            $datedAnimal .
            $animalDate .
            self::idSql($ids, 'id') .
            '), entered AS (SELECT animal_id,MIN(entry_date) entry_date FROM entry_sources GROUP BY animal_id) ';
        $colors = $rows(
            $entered .
                "SELECT COALESCE(NULLIF(TRIM(a.color),''),'Non renseignée') label,COUNT(*) total FROM entered e JOIN animals a ON a.id=e.animal_id GROUP BY label ORDER BY total DESC,label",
            $p,
        );
        $breeds = $rows(
            $entered .
                "SELECT COALESCE(NULLIF(TRIM(a.breed),''),'Non renseignée') label,COUNT(*) total FROM entered e JOIN animals a ON a.id=e.animal_id GROUP BY label ORDER BY total DESC,label",
            $p,
        );
        $species = $rows(
            $entered .
                "SELECT CASE lower(a.species) WHEN 'cat' THEN 'Chats' WHEN 'chat' THEN 'Chats' WHEN 'dog' THEN 'Chiens' WHEN 'chien' THEN 'Chiens' ELSE COALESCE(NULLIF(TRIM(a.species),''),'Inconnue') END label,COUNT(*) total FROM entered e JOIN animals a ON a.id=e.animal_id GROUP BY label ORDER BY total DESC",
            $p,
        );
        $intakeTypes = $rows(
            $entered .
                "SELECT COALESCE(NULLIF(a.intake_type,''),'unknown') label,COUNT(*) total FROM entered e JOIN animals a ON a.id=e.animal_id GROUP BY label ORDER BY total DESC",
            $p,
        );
        $intakeReasons = $rows(
            $entered .
                "SELECT CASE WHEN a.intake_owner_care_home=1 THEN 'owner_care_home' ELSE COALESCE(NULLIF(a.intake_reason,''),'unknown') END label,COUNT(*) total FROM entered e JOIN animals a ON a.id=e.animal_id GROUP BY label ORDER BY total DESC",
            $p,
        );
        if ($year === null) {
            $animalFilter = self::idSql($ids, 'a.id');
            $details = [];
            $details['sex'] = $rows(
                "SELECT CASE a.sex WHEN 'F' THEN 'Femelles' WHEN 'M' THEN 'Mâles' ELSE 'Inconnu' END label,COUNT(*) total FROM animals a WHERE a.deleted_at IS NULL" .
                    $animalFilter .
                    ' GROUP BY a.sex ORDER BY total DESC',
            );
            $details['ages'] = $rows(
                "SELECT CASE WHEN a.birth_date IS NULL OR NULLIF(TRIM(a.intake_date),'') IS NULL THEN 'Non renseigné' WHEN (julianday(a.intake_date)-julianday(a.birth_date))/365.25<1 THEN 'Moins d’un an' WHEN (julianday(a.intake_date)-julianday(a.birth_date))/365.25<3 THEN '1 à 2 ans' WHEN (julianday(a.intake_date)-julianday(a.birth_date))/365.25<8 THEN '3 à 7 ans' ELSE '8 ans et plus' END label,COUNT(*) total FROM animals a WHERE a.deleted_at IS NULL" .
                    $animalFilter .
                    ' GROUP BY label ORDER BY total DESC',
            );
            $details['deathCauses'] = $rows(
                "SELECT COALESCE(NULLIF(d.cause_code,''),'unknown') label,COUNT(*) total FROM animal_deaths d JOIN animals a ON a.id=d.animal_id WHERE a.deleted_at IS NULL" .
                    self::idSql($ids, 'd.animal_id') .
                    ' GROUP BY label ORDER BY total DESC',
            );
            $details['stays'] = $rows(
                "SELECT CASE WHEN julianday(ad.adoption_date)-julianday(COALESCE(NULLIF(a.intake_date,''),date(a.created_at)))<30 THEN 'Moins d’un mois' WHEN julianday(ad.adoption_date)-julianday(COALESCE(NULLIF(a.intake_date,''),date(a.created_at)))<90 THEN '1 à 3 mois' WHEN julianday(ad.adoption_date)-julianday(COALESCE(NULLIF(a.intake_date,''),date(a.created_at)))<180 THEN '3 à 6 mois' WHEN julianday(ad.adoption_date)-julianday(COALESCE(NULLIF(a.intake_date,''),date(a.created_at)))<365 THEN '6 à 12 mois' ELSE 'Plus d’un an' END label,COUNT(*) total FROM adoptions ad JOIN animals a ON a.id=ad.animal_id WHERE a.deleted_at IS NULL AND ad.adoption_date IS NOT NULL AND date(ad.adoption_date)>=date(COALESCE(NULLIF(a.intake_date,''),date(a.created_at)))" .
                    $animalFilter .
                    ' GROUP BY label ORDER BY total DESC',
            );
            $origins = $municipality !== null ? [$municipality => (int) array_sum($species)] : self::municipalities();
        } elseif ($ids !== null) {
            $details = self::details($year, $ids);
            $origins = $municipality !== null ? [$municipality => (int) array_sum($species)] : [];
        } else {
            $details = GrantService::detailedStatistics($year);
            $origins = $details['origins'];
        }
        foreach ($details as &$items) {
            arsort($items, SORT_NUMERIC);
        }
        unset($items);
        $filter = self::idSql($ids, 'id');
        $currentStatus = $rows(
            "SELECT CASE status WHEN 'refuge' THEN 'Refuge' WHEN 'fa' THEN 'Famille d’accueil' WHEN 'fa_permanente' THEN 'Accueil permanent' WHEN 'chat_libre' THEN 'Chats libres suivis' WHEN 'quarantaine' THEN 'Quarantaine' WHEN 'soin' THEN 'Infirmerie' WHEN 'reserve' THEN 'Réservé' ELSE status END label,COUNT(*) total FROM animals WHERE deleted_at IS NULL AND archived_at IS NULL AND lower(status) NOT IN ('adopte','adopté','decede','décédé')" .
                $filter .
                ' GROUP BY status ORDER BY total DESC',
        );
        $freeCatSites = $rows(
            "SELECT COALESCE(NULLIF(TRIM(free_cat_site_name),''),NULLIF(TRIM(free_cat_city),''),'Site non renseigné') label,COUNT(*) total FROM animals WHERE deleted_at IS NULL AND archived_at IS NULL AND status='chat_libre'" .
                $filter .
                ' GROUP BY label ORDER BY total DESC,label',
        );
        $releaseSql =
            $year === null
                ? "SELECT substr(free_cat_released_at,1,4) label,COUNT(*) total FROM animals WHERE deleted_at IS NULL AND free_cat_released_at IS NOT NULL AND trim(free_cat_released_at)<>''" .
                    $filter .
                    ' GROUP BY label ORDER BY label DESC'
                : "SELECT COALESCE(NULLIF(TRIM(free_cat_site_name),''),NULLIF(TRIM(free_cat_city),''),'Site non renseigné') label,COUNT(*) total FROM animals WHERE deleted_at IS NULL AND date(free_cat_released_at) BETWEEN :start AND :end" .
                    $filter .
                    ' GROUP BY label ORDER BY total DESC,label';
        $freeCatReleases = $rows($releaseSql, $p);
        return [
            'colors' => $colors,
            'breeds' => $breeds,
            'species' => $species,
            'intakeTypes' => $intakeTypes,
            'intakeReasons' => $intakeReasons,
            'sex' => $details['sex'],
            'ages' => $details['ages'],
            'origins' => $origins,
            'deathCauses' => $details['deathCauses'],
            'stays' => $details['stays'],
            'currentStatus' => $currentStatus,
            'freeCatSites' => $freeCatSites,
            'freeCatReleases' => $freeCatReleases,
        ];
    }

    public static function currentHealth(?array $ids = null): array
    {
        $sql =
            "SELECT COUNT(*) total,SUM(CASE WHEN status='chat_libre' THEN 1 ELSE 0 END) free_cats,SUM(CASE WHEN sterilization_status='yes' THEN 1 ELSE 0 END) sterilized,SUM(CASE WHEN sterilization_status='no' THEN 1 ELSE 0 END) unsterilized,SUM(CASE WHEN COALESCE(NULLIF(sterilization_status,''),'unknown')='unknown' THEN 1 ELSE 0 END) sterilization_unknown,SUM(CASE WHEN COALESCE(NULLIF(TRIM(chip_id),''),NULLIF(TRIM(chip),'')) IS NOT NULL THEN 1 ELSE 0 END) chipped,SUM(CASE WHEN fiv_status='positive' THEN 1 ELSE 0 END) fiv_positive,SUM(CASE WHEN felv_status='positive' THEN 1 ELSE 0 END) felv_positive FROM animals WHERE deleted_at IS NULL AND archived_at IS NULL AND lower(status) NOT IN ('adopte','adopté','decede','décédé')" .
            self::idSql($ids, 'id');
        $row = DB::pdo()->query($sql)->fetch(PDO::FETCH_ASSOC) ?: [];
        return array_map('intval', $row);
    }
}
