<?php

declare(strict_types=1);
final class AgendaService
{
    public const TYPES = ['appointment', 'veterinary', 'transport', 'pre_adoption', 'task', 'other'];
    public static function save(array $data, ?int $id = null): int
    {
        $type = (string) ($data['item_type'] ?? 'task');
        $title = trim((string) ($data['title'] ?? ''));
        $date = trim((string) ($data['date'] ?? ''));
        $time = trim((string) ($data['time'] ?? ''));
        $allDay = isset($data['all_day']);
        if (!in_array($type, self::TYPES, true) || $title === '' || !self::date($date)) {
            throw new RuntimeException(t('agenda.invalid'));
        }
        if (!$allDay && !preg_match('/^([01]\d|2[0-3]):[0-5]\d$/', $time)) {
            throw new RuntimeException(t('agenda.invalid_time'));
        }
        $start = $date . ' ' . ($allDay ? '00:00:00' : $time . ':00');
        $endDate = trim((string) ($data['end_date'] ?? ''));
        $endTime = trim((string) ($data['end_time'] ?? ''));
        $end = null;
        if ($endDate !== '' || $endTime !== '') {
            if (!self::date($endDate) || !preg_match('/^([01]\d|2[0-3]):[0-5]\d$/', $endTime)) {
                throw new RuntimeException(t('agenda.invalid_end'));
            }
            $end = $endDate . ' ' . $endTime . ':00';
            if ($end < $start) {
                throw new RuntimeException(t('agenda.end_before_start'));
            }
        }
        $repeat = (string) ($data['repeat_rule'] ?? 'none');
        if (!in_array($repeat, ['none', 'daily', 'weekly', 'monthly'], true)) {
            $repeat = 'none';
        }
        $until = trim((string) ($data['repeat_until'] ?? ''));
        if ($until !== '' && !self::date($until)) {
            throw new RuntimeException(t('agenda.invalid_repeat'));
        }
        $values = [
            ':type' => $type,
            ':title' => mb_substr($title, 0, 180),
            ':description' => trim((string) ($data['description'] ?? '')) ?: null,
            ':start' => $start,
            ':end' => $end,
            ':all_day' => $allDay ? 1 : 0,
            ':location' => trim((string) ($data['location'] ?? '')) ?: null,
            ':priority' => in_array(
                $p = (string) ($data['priority'] ?? 'normal'),
                ['low', 'normal', 'high', 'urgent'],
                true,
            )
                ? $p
                : 'normal',
            ':assigned' => (int) ($data['assigned_user_id'] ?? 0) ?: null,
            ':animal' => (int) ($data['animal_id'] ?? 0) ?: null,
            ':contact' => (int) ($data['contact_id'] ?? 0) ?: null,
            ':reminder' => ($data['reminder_minutes'] ?? '') === '' ? null : max(0, (int) $data['reminder_minutes']),
            ':repeat' => $repeat,
            ':until' => $until ?: null,
            ':user' => Auth::id(),
        ];
        $db = DB::pdo();
        $db->beginTransaction();
        try {
            if ($id) {
                $values[':id'] = $id;
                $db->prepare(
                    "UPDATE agenda_items SET item_type=:type,title=:title,description=:description,starts_at=:start,ends_at=:end,all_day=:all_day,location=:location,priority=:priority,assigned_user_id=:assigned,animal_id=:animal,contact_id=:contact,reminder_minutes=:reminder,repeat_rule=:repeat,repeat_until=:until,updated_at=datetime('now') WHERE id=:id",
                )->execute($values);
            } else {
                $db->prepare(
                    'INSERT INTO agenda_items(item_type,title,description,starts_at,ends_at,all_day,location,priority,assigned_user_id,animal_id,contact_id,reminder_minutes,repeat_rule,repeat_until,created_by) VALUES(:type,:title,:description,:start,:end,:all_day,:location,:priority,:assigned,:animal,:contact,:reminder,:repeat,:until,:user)',
                )->execute($values);
                $id = (int) $db->lastInsertId();
            }
            self::saveVolunteers($id, $data['volunteer_contact_ids'] ?? []);
            $db->commit();
            return $id;
        } catch (Throwable $e) {
            if ($db->inTransaction()) {
                $db->rollBack();
            }
            throw $e;
        }
    }
    public static function find(int $id): ?array
    {
        $s = DB::pdo()->prepare(
            "SELECT ai.*,(SELECT group_concat(contact_id,',') FROM agenda_volunteers WHERE agenda_item_id=ai.id) volunteer_ids FROM agenda_items ai WHERE ai.id=?",
        );
        $s->execute([$id]);
        return $s->fetch(PDO::FETCH_ASSOC) ?: null;
    }
    public static function setStatus(int $id, string $status): void
    {
        if (!in_array($status, ['planned', 'completed', 'cancelled'], true)) {
            throw new RuntimeException(t('agenda.invalid'));
        }
        DB::pdo()
            ->prepare(
                "UPDATE agenda_items SET status=?,completed_at=CASE WHEN ?='completed' THEN datetime('now') ELSE NULL END,completed_by=CASE WHEN ?='completed' THEN ? ELSE NULL END,updated_at=datetime('now') WHERE id=?",
            )
            ->execute([$status, $status, $status, Auth::id(), $id]);
    }
    public static function conflicts(int $id): array
    {
        $db = DB::pdo();
        $s = $db->prepare(
            "SELECT DISTINCT other.id,other.title,other.starts_at FROM agenda_items current JOIN agenda_items other ON other.id<>current.id AND other.status='planned' AND datetime(other.starts_at)<datetime(COALESCE(current.ends_at,CASE WHEN current.all_day=1 THEN datetime(current.starts_at,'+1 day') ELSE datetime(current.starts_at,'+1 hour') END)) AND datetime(COALESCE(other.ends_at,CASE WHEN other.all_day=1 THEN datetime(other.starts_at,'+1 day') ELSE datetime(other.starts_at,'+1 hour') END))>datetime(current.starts_at) WHERE current.id=:id AND current.status='planned' AND ((current.assigned_user_id IS NOT NULL AND other.assigned_user_id=current.assigned_user_id) OR EXISTS(SELECT 1 FROM agenda_volunteers mine JOIN agenda_volunteers theirs ON theirs.contact_id=mine.contact_id AND theirs.agenda_item_id=other.id WHERE mine.agenda_item_id=current.id)) ORDER BY datetime(other.starts_at)",
        );
        $s->execute([':id' => $id]);
        return $s->fetchAll(PDO::FETCH_ASSOC);
    }
    public static function month(DateTimeImmutable $month): array
    {
        return self::between(
            $month->modify('first day of this month')->setTime(0, 0),
            $month->modify('last day of this month')->setTime(23, 59, 59),
        );
    }
    public static function week(DateTimeImmutable $date): array
    {
        $start = $date->modify('monday this week')->setTime(0, 0);
        return self::between($start, $start->modify('+6 days')->setTime(23, 59, 59));
    }
    public static function range(DateTimeImmutable $from, DateTimeImmutable $to): array
    {
        return self::between($from, $to);
    }
    public static function upcoming(int $days = 30): array
    {
        return self::between(new DateTimeImmutable('today'), new DateTimeImmutable('+' . $days . ' days 23:59:59'));
    }
    public static function due(): array
    {
        $db = DB::pdo();
        $overdue = $db
            ->query(
                "SELECT ai.*,ai.starts_at instance_starts_at,a.name animal_name,COALESCE(u.display_name,u.username) assigned_name FROM agenda_items ai LEFT JOIN animals a ON a.id=ai.animal_id LEFT JOIN users u ON u.id=ai.assigned_user_id WHERE ai.status='planned' AND ai.repeat_rule='none' AND datetime(ai.starts_at)<datetime('now') ORDER BY datetime(ai.starts_at) LIMIT 25",
            )
            ->fetchAll(PDO::FETCH_ASSOC);
        $soon = array_values(
            array_filter(
                self::upcoming(2),
                static fn($row) => $row['status'] === 'planned' &&
                    strtotime($row['instance_starts_at']) <= strtotime('+1 day'),
            ),
        );
        $seen = [];
        $out = [];
        foreach ([...$overdue, ...$soon] as $row) {
            $key = (int) $row['id'] . '|' . ($row['instance_starts_at'] ?? $row['starts_at']);
            if (isset($seen[$key])) {
                continue;
            }
            $seen[$key] = true;
            $out[] = $row;
        }
        usort(
            $out,
            static fn($a, $b) => strcmp(
                (string) ($a['instance_starts_at'] ?? $a['starts_at']),
                (string) ($b['instance_starts_at'] ?? $b['starts_at']),
            ),
        );
        return array_slice($out, 0, 25);
    }
    private static function between(DateTimeImmutable $from, DateTimeImmutable $to): array
    {
        $db = DB::pdo();
        $s = $db->prepare(
            "SELECT ai.*,a.name animal_name,dc.name contact_name,COALESCE(u.display_name,u.username) assigned_name,(SELECT group_concat(vdc.name,', ') FROM agenda_volunteers av JOIN directory_contacts vdc ON vdc.id=av.contact_id WHERE av.agenda_item_id=ai.id) volunteer_names,(SELECT group_concat(av.contact_id,',') FROM agenda_volunteers av WHERE av.agenda_item_id=ai.id) volunteer_ids FROM agenda_items ai LEFT JOIN animals a ON a.id=ai.animal_id LEFT JOIN directory_contacts dc ON dc.id=ai.contact_id LEFT JOIN users u ON u.id=ai.assigned_user_id WHERE date(ai.starts_at)<=:to AND (ai.repeat_rule<>'none' OR datetime(ai.starts_at)>=:from) AND ai.status<>'cancelled' ORDER BY datetime(ai.starts_at),ai.priority DESC",
        );
        $s->execute([':from' => $from->format('Y-m-d H:i:s'), ':to' => $to->format('Y-m-d')]);
        $out = [];
        foreach ($s->fetchAll(PDO::FETCH_ASSOC) as $row) {
            $start = new DateTimeImmutable($row['starts_at']);
            $end = $row['ends_at'] ? new DateTimeImmutable($row['ends_at']) : null;
            $step = match ($row['repeat_rule']) {
                'daily' => '+1 day',
                'weekly' => '+1 week',
                'monthly' => '+1 month',
                default => null,
            };
            while ($start < $from && $step) {
                $start = $start->modify($step);
            }
            do {
                if ($start > $to) {
                    break;
                }
                if (!$row['repeat_until'] || $start->format('Y-m-d') <= $row['repeat_until']) {
                    $instance = $row;
                    $instance['instance_starts_at'] = $start->format('Y-m-d H:i:s');
                    $instance['instance_ends_at'] = $end
                        ? $end
                            ->modify(
                                $start->getTimestamp() -
                                    (new DateTimeImmutable($row['starts_at']))->getTimestamp() .
                                    ' seconds',
                            )
                            ->format('Y-m-d H:i:s')
                        : null;
                    $instance['source_type'] = 'agenda';
                    if (!empty($instance['volunteer_names'])) {
                        $instance['title'] .= ' · 👥 ' . $instance['volunteer_names'];
                    }
                    $out[] = $instance;
                }
                if (!$step) {
                    break;
                }
                $start = $start->modify($step);
            } while (!$row['repeat_until'] || $start->format('Y-m-d') <= $row['repeat_until']);
        }
        $vaccines = $db->prepare(
            "SELECT v.id,v.due_date,rv.name vaccine_name,a.id animal_id,a.name animal_name FROM vaccinations v JOIN ref_vaccines rv ON rv.id=v.vaccine_id JOIN animals a ON a.id=v.animal_id WHERE v.due_date IS NOT NULL AND date(v.due_date) BETWEEN :from AND :to AND a.deleted_at IS NULL AND a.archived_at IS NULL AND lower(a.status) NOT IN ('adopte','adopté','decede','décédé') AND NOT EXISTS(SELECT 1 FROM vaccinations newer WHERE newer.animal_id=v.animal_id AND newer.vaccine_id=v.vaccine_id AND date(newer.done_date)>date(v.done_date)) ORDER BY date(v.due_date),a.name COLLATE NOCASE",
        );
        $vaccines->execute([':from' => $from->format('Y-m-d'), ':to' => $to->format('Y-m-d')]);
        foreach ($vaccines->fetchAll(PDO::FETCH_ASSOC) as $v) {
            $out[] = self::reminder(
                -(100000000 + (int) $v['id']),
                (int) $v['id'],
                'vaccine',
                'vaccine',
                t('agenda.vaccine_reminder', ['vaccine' => $v['vaccine_name']]),
                $v['due_date'],
                (int) $v['animal_id'],
                $v['animal_name'],
                'high',
            );
        }

        $dewormings = $db->prepare(
            "SELECT d.id,d.next_due_date,COALESCE(rd.name,'') dewormer_name,a.id animal_id,a.name animal_name FROM dewormings d LEFT JOIN ref_dewormers rd ON rd.id=d.dewormer_id JOIN animals a ON a.id=d.animal_id WHERE d.next_due_date IS NOT NULL AND date(d.next_due_date) BETWEEN :from AND :to AND a.deleted_at IS NULL AND a.archived_at IS NULL AND lower(a.status) NOT IN ('adopte','adopté','decede','décédé') AND NOT EXISTS(SELECT 1 FROM dewormings newer WHERE newer.animal_id=d.animal_id AND (date(newer.administered_on)>date(d.administered_on) OR (date(newer.administered_on)=date(d.administered_on) AND newer.id>d.id))) ORDER BY date(d.next_due_date),a.name COLLATE NOCASE",
        );
        $dewormings->execute([':from' => $from->format('Y-m-d'), ':to' => $to->format('Y-m-d')]);
        foreach ($dewormings->fetchAll(PDO::FETCH_ASSOC) as $d) {
            $out[] = self::reminder(
                -(200000000 + (int) $d['id']),
                (int) $d['id'],
                'deworming',
                'deworming',
                t('agenda.deworming_reminder', ['product' => $d['dewormer_name'] ?: t('agenda.deworming')]),
                $d['next_due_date'],
                (int) $d['animal_id'],
                $d['animal_name'],
                'high',
            );
        }

        $treatments = $db->prepare(
            "SELECT t.id,t.start_date,t.end_date,m.name medication_name,a.id animal_id,a.name animal_name FROM treatments t JOIN ref_medications m ON m.id=t.medication_id JOIN animals a ON a.id=t.animal_id WHERE t.ongoing=1 AND (date(t.start_date) BETWEEN :from AND :to OR (t.end_date IS NOT NULL AND date(t.end_date) BETWEEN :from AND :to)) AND a.deleted_at IS NULL AND a.archived_at IS NULL AND lower(a.status) NOT IN ('adopte','adopté','decede','décédé') ORDER BY date(t.start_date),a.name COLLATE NOCASE",
        );
        $treatments->execute([':from' => $from->format('Y-m-d'), ':to' => $to->format('Y-m-d')]);
        foreach ($treatments->fetchAll(PDO::FETCH_ASSOC) as $t) {
            if ($t['start_date'] >= $from->format('Y-m-d') && $t['start_date'] <= $to->format('Y-m-d')) {
                $out[] = self::reminder(
                    -(300000000 + (int) $t['id']),
                    (int) $t['id'],
                    'treatment_start',
                    'treatment_start',
                    t('agenda.treatment_start', ['medication' => $t['medication_name']]),
                    $t['start_date'],
                    (int) $t['animal_id'],
                    $t['animal_name'],
                    'high',
                );
            }
            if ($t['end_date'] && $t['end_date'] >= $from->format('Y-m-d') && $t['end_date'] <= $to->format('Y-m-d')) {
                $out[] = self::reminder(
                    -(400000000 + (int) $t['id']),
                    (int) $t['id'],
                    'treatment_end',
                    'treatment_end',
                    t('agenda.treatment_end', ['medication' => $t['medication_name']]),
                    $t['end_date'],
                    (int) $t['animal_id'],
                    $t['animal_name'],
                    'high',
                );
            }
        }
        usort($out, static fn($a, $b) => strcmp($a['instance_starts_at'], $b['instance_starts_at']));
        return $out;
    }
    private static function reminder(
        int $id,
        int $sourceId,
        string $sourceType,
        string $itemType,
        string $title,
        string $date,
        int $animalId,
        string $animalName,
        string $priority,
    ): array {
        return [
            'id' => $id,
            'source_id' => $sourceId,
            'source_type' => $sourceType,
            'item_type' => $itemType,
            'title' => $title,
            'description' => null,
            'starts_at' => $date . ' 00:00:00',
            'instance_starts_at' => $date . ' 00:00:00',
            'instance_ends_at' => null,
            'all_day' => 1,
            'location' => null,
            'status' => 'planned',
            'priority' => $priority,
            'assigned_user_id' => null,
            'assigned_name' => null,
            'volunteer_names' => null,
            'volunteer_ids' => null,
            'animal_id' => $animalId,
            'animal_name' => $animalName,
            'contact_id' => null,
            'contact_name' => null,
            'reminder_minutes' => null,
            'repeat_rule' => 'none',
            'repeat_until' => null,
        ];
    }
    private static function saveVolunteers(int $itemId, mixed $input): void
    {
        $ids = is_array($input) ? array_values(array_unique(array_filter(array_map('intval', $input)))) : [];
        $db = DB::pdo();
        $db->prepare('DELETE FROM agenda_volunteers WHERE agenda_item_id=?')->execute([$itemId]);
        if (!$ids) {
            return;
        }
        $check = $db->prepare(
            "SELECT 1 FROM directory_contacts dc JOIN directory_contact_roles r ON r.contact_id=dc.id AND r.role='benevole' WHERE dc.id=? AND dc.deleted_at IS NULL",
        );
        $insert = $db->prepare('INSERT INTO agenda_volunteers(agenda_item_id,contact_id) VALUES(?,?)');
        foreach ($ids as $id) {
            $check->execute([$id]);
            if ($check->fetchColumn()) {
                $insert->execute([$itemId, $id]);
            }
        }
    }
    private static function date(string $v): bool
    {
        $d = DateTimeImmutable::createFromFormat('!Y-m-d', $v);
        return $d && $d->format('Y-m-d') === $v;
    }
}
