<?php

namespace App\Services;

use App\Models\NotificationModel;
use App\Models\TaskModel;
use App\Support\AppClock;
use DateTimeImmutable;
use DateTimeZone;
use Throwable;

/**
 * In-app due notifications (Phase 10). No Firebase/FCM, push, SMS, or email.
 *
 * Timezone: due evaluation and created_at use App\Support\AppClock
 * (Config\App::$appTimezone). Dashboard and recurrence use the same clock.
 *
 * Duplicate identity: unique (user_id, occurrence_id, kind='due'). Read/unread
 * does not change that identity, so a read due notification is never recreated.
 *
 * Deleted tasks: notifications are retained (FKs SET NULL on hard-delete).
 * Opening a notification whose task is gone shows a friendly error instead of
 * a broken edit page. Completing an occurrence after a notification exists
 * leaves that historical row; it is not duplicated.
 */
class NotificationService
{
    public const KIND_DUE = 'due';

    public const TITLE_DUE = 'Task Due';

    public const MSG_NOT_FOUND = 'That notification could not be found.';
    public const MSG_TASK_GONE = 'That task is no longer available.';
    public const MSG_EMPTY     = 'No notifications yet.';
    public const MSG_READ      = 'Notification marked as read.';
    public const MSG_ALL_READ  = 'All notifications marked as read.';

    /** Y-m-d H:i:s override so tests can freeze "now" in app timezone. */
    public ?string $nowOverride = null;

    public function __construct(
        private readonly NotificationModel $notifications = new NotificationModel(),
        private readonly TaskModel $tasks = new TaskModel(),
    ) {
    }

    public function timezone(): DateTimeZone
    {
        return AppClock::timezone();
    }

    public function now(): DateTimeImmutable
    {
        $tz = $this->timezone();
        if ($this->nowOverride !== null && $this->nowOverride !== '') {
            $parsed = DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $this->nowOverride, $tz);
            if ($parsed instanceof DateTimeImmutable) {
                return $parsed;
            }

            return AppClock::parse($this->nowOverride);
        }

        return AppClock::now();
    }

    public static function formatBadge(int $count): string
    {
        if ($count <= 0) {
            return '';
        }
        if ($count > 99) {
            return '99+';
        }

        return (string) $count;
    }

    public function unreadCount(int $userId): int
    {
        if ($userId < 1) {
            return 0;
        }

        return $this->notifications->unreadCountForUser($userId);
    }

    /**
     * @return list<array<string, mixed>>
     */
    public function listForUser(int $userId): array
    {
        $rows = $this->notifications
            ->where('user_id', $userId)
            ->orderBy('created_at', 'DESC')
            ->orderBy('id', 'DESC')
            ->findAll();

        $presented = [];
        foreach ($rows as $row) {
            $presented[] = $this->present($row);
        }

        return $presented;
    }

    /**
     * Create missing due notifications for every active user. Safe to run often.
     */
    public function generateDueNotifications(): int
    {
        $now     = $this->now();
        $today   = $now->format('Y-m-d');
        $nowTime = $now->format('H:i:s');
        $createdAt = $now->format('Y-m-d H:i:s');

        $db = db_connect();
        $rows = $db->table('task_occurrences as o')
            ->select('o.id as occurrence_id, o.occurrence_date, o.occurrence_time, t.id as task_id, t.user_id, t.title')
            ->join('tasks as t', 't.id = o.task_id')
            ->join('categories as c', 'c.id = t.category_id AND c.user_id = t.user_id')
            ->join('users as u', 'u.id = t.user_id')
            ->join(
                'notifications as n',
                'n.occurrence_id = o.id AND n.user_id = t.user_id AND n.kind = ' . $db->escape(self::KIND_DUE),
                'left',
            )
            ->where('u.is_active', 1)
            ->where('t.deleted_at', null)
            ->where('c.deleted_at', null)
            ->where('o.is_deleted', 0)
            ->where('o.status', 'pending')
            ->where('o.occurrence_date IS NOT NULL', null, false)
            ->where('n.id', null)
            ->groupStart()
                ->where('o.occurrence_date <', $today)
                ->orGroupStart()
                    ->where('o.occurrence_date', $today)
                    ->where('o.occurrence_time', null)
                ->groupEnd()
                ->orGroupStart()
                    ->where('o.occurrence_date', $today)
                    ->where('o.occurrence_time IS NOT NULL', null, false)
                    ->where('o.occurrence_time <=', $nowTime)
                ->groupEnd()
            ->groupEnd()
            ->get()
            ->getResultArray();

        $created = 0;
        foreach ($rows as $row) {
            if ($this->insertDue($row, $today, $createdAt)) {
                $created++;
            }
        }

        return $created;
    }

    /**
     * @return array{ok: bool, message: string, status?: int, notification?: array<string, mixed>}
     */
    public function markRead(int $userId, int $notificationId): array
    {
        $row = $this->notifications->findOwned($userId, $notificationId);
        if ($row === null) {
            return ['ok' => false, 'message' => self::MSG_NOT_FOUND, 'status' => 404];
        }

        if ((int) $row['is_read'] === 1) {
            return ['ok' => true, 'message' => self::MSG_READ, 'notification' => $row];
        }

        $ok = $this->notifications->update($notificationId, [
            'is_read' => 1,
            'read_at' => $this->now()->format('Y-m-d H:i:s'),
        ]);
        if ($ok === false) {
            return ['ok' => false, 'message' => self::MSG_NOT_FOUND, 'status' => 500];
        }

        $updated = $this->notifications->findOwned($userId, $notificationId);

        return [
            'ok'           => true,
            'message'      => self::MSG_READ,
            'notification' => $updated ?? $row,
        ];
    }

    public function markAllRead(int $userId): int
    {
        if ($userId < 1) {
            return 0;
        }

        $now = $this->now()->format('Y-m-d H:i:s');
        db_connect()->table('notifications')
            ->where('user_id', $userId)
            ->where('is_read', 0)
            ->update([
                'is_read' => 1,
                'read_at' => $now,
            ]);

        return db_connect()->affectedRows();
    }

    /**
     * Mark read, then describe where to send the user. Never follows a stored URL.
     *
     * @return array{ok: bool, message: string, status?: int, redirect?: string, flash?: string}
     */
    public function open(int $userId, int $notificationId): array
    {
        $marked = $this->markRead($userId, $notificationId);
        if (! ($marked['ok'] ?? false)) {
            return $marked;
        }

        $row    = $marked['notification'] ?? [];
        $taskId = (int) ($row['task_id'] ?? 0);
        if ($taskId < 1) {
            return [
                'ok'       => true,
                'message'  => self::MSG_TASK_GONE,
                'redirect' => '/notifications',
                'flash'    => self::MSG_TASK_GONE,
            ];
        }

        $task = $this->tasks->findOwned($userId, $taskId);
        if ($task === null) {
            return [
                'ok'       => true,
                'message'  => self::MSG_TASK_GONE,
                'redirect' => '/notifications',
                'flash'    => self::MSG_TASK_GONE,
            ];
        }

        return [
            'ok'       => true,
            'message'  => self::MSG_READ,
            'redirect' => '/tasks/' . $taskId . '/edit',
        ];
    }

    /**
     * @param array<string, mixed> $row
     *
     * @return array<string, mixed>
     */
    public function present(array $row): array
    {
        $created = (string) ($row['created_at'] ?? '');
        $label   = '';
        if ($created !== '') {
            $dt = DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $created, $this->timezone());
            if ($dt instanceof DateTimeImmutable) {
                $label = $dt->format('j M Y, g:i A');
            } else {
                $label = $created;
            }
        }

        return [
            'id'            => (int) ($row['id'] ?? 0),
            'title'         => (string) ($row['title'] ?? ''),
            'message'       => (string) ($row['message'] ?? ''),
            'is_read'       => (int) ($row['is_read'] ?? 0) === 1,
            'task_id'       => isset($row['task_id']) && $row['task_id'] !== null ? (int) $row['task_id'] : null,
            'occurrence_id' => isset($row['occurrence_id']) && $row['occurrence_id'] !== null ? (int) $row['occurrence_id'] : null,
            'created_at'    => $created,
            'created_label' => $label,
        ];
    }

    /**
     * @param array<string, mixed> $row
     */
    private function insertDue(array $row, string $today, string $createdAt): bool
    {
        $occurrenceId = (int) ($row['occurrence_id'] ?? 0);
        $userId       = (int) ($row['user_id'] ?? 0);
        $taskId       = (int) ($row['task_id'] ?? 0);
        if ($occurrenceId < 1 || $userId < 1 || $taskId < 1) {
            return false;
        }

        $date = (string) ($row['occurrence_date'] ?? '');
        $time = $row['occurrence_time'] ?? null;
        $time = is_string($time) && $time !== '' ? $time : null;

        $payload = [
            'user_id'       => $userId,
            'task_id'       => $taskId,
            'occurrence_id' => $occurrenceId,
            'kind'          => self::KIND_DUE,
            'title'         => self::TITLE_DUE,
            'message'       => $this->dueMessage((string) ($row['title'] ?? ''), $time, $date, $today),
            'is_read'       => 0,
            'read_at'       => null,
            'created_at'    => $createdAt,
        ];

        try {
            $db = db_connect();
            $db->table('notifications')->ignore(true)->insert($payload);

            return $db->affectedRows() === 1;
        } catch (Throwable) {
            return false;
        }
    }

    private function dueMessage(string $title, ?string $time, string $date, string $today): string
    {
        $title = trim($title);
        if ($title === '') {
            $title = 'A task';
        }

        if ($time !== null) {
            return $title . ' is due now.';
        }

        if ($date !== '' && $date < $today) {
            return $title . ' is overdue.';
        }

        return $title . ' is due today.';
    }
}
