<?php

declare(strict_types=1);

namespace PpPhp\Core;

/**
 * Notification API – manage in-app notifications with auto‑table creation.
 *
 * Usage:
 *   Notification::Enable('notifications');      // creates table + stores config
 *   Notification::Add(['message'=>'New record added', 'userId'=>5]);
 *   $unread = Notification::Get(5, 10, true);
 *   Notification::MarkRead(12);
 *   Notification::MarkAllRead(5);
 *   $count = Notification::Count(5);
 *   Notification::DeleteOld(30);
 */
class Notification
{
    /**
     * Enable notifications – creates the table if it doesn't exist, stores config.
     */
    public static function Enable(string $tableName = 'notifications'): void
    {
        // The project database is already initialised by the controller.
        // We just need to create the table if it doesn't exist.
        $sql = "CREATE TABLE IF NOT EXISTS `{$tableName}` (
        `id` INT AUTO_INCREMENT PRIMARY KEY,
        `message` TEXT NOT NULL,
        `title` VARCHAR(255) DEFAULT NULL,
        `icon` VARCHAR(50) DEFAULT NULL,
        `url` VARCHAR(500) DEFAULT NULL,
        `user_id` INT DEFAULT NULL,
        `type` VARCHAR(20) DEFAULT 'info',
        `permissions` VARCHAR(500) DEFAULT NULL,
        `expire_at` DATETIME DEFAULT NULL,
        `is_read` TINYINT(1) DEFAULT 0,
        `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
        INDEX (`user_id`),
        INDEX (`is_read`),
        INDEX (`created_at`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4";
        DB::Exec($sql);
        self::ensureColumns();
    }

    /** Notification severities → their default Bootstrap icon + semantic colour. */
    public const TYPES = ['info', 'success', 'warning', 'error'];
    private const TYPE_ICON = ['info' => 'info-circle', 'success' => 'check-circle', 'warning' => 'exclamation-triangle', 'error' => 'x-circle'];

    /** The default icon for a severity (used when no explicit icon is given). */
    public static function typeIcon(string $type): string
    {
        return self::TYPE_ICON[$type] ?? 'bell';
    }

    /**
     * Self-heal: backfill any columns the current table is missing, so the rich
     * API (title/icon/url/type/…) works even when the table was first created by
     * a leaner writer (e.g. the Calendar's own minimal notifications table).
     * Runs at most once per request.
     */
    private static bool $columnsChecked = false;
    private static function ensureColumns(): void
    {
        if (self::$columnsChecked) return;
        self::$columnsChecked = true;
        $t = self::getTableName();
        try {
            $rows = DB::FetchAll("SHOW COLUMNS FROM `{$t}`") ?: [];
            if ($rows === []) return; // table not created yet
            $have = array_map(static fn($c) => (string) ($c['Field'] ?? ''), $rows);
            $want = [
                'title'       => "ADD COLUMN `title` VARCHAR(255) DEFAULT NULL",
                'icon'        => "ADD COLUMN `icon` VARCHAR(50) DEFAULT NULL",
                'url'         => "ADD COLUMN `url` VARCHAR(500) DEFAULT NULL",
                'type'        => "ADD COLUMN `type` VARCHAR(20) DEFAULT 'info'",
                'permissions' => "ADD COLUMN `permissions` VARCHAR(500) DEFAULT NULL",
                'expire_at'   => "ADD COLUMN `expire_at` DATETIME DEFAULT NULL",
            ];
            $alters = [];
            foreach ($want as $col => $ddl) {
                if (!in_array($col, $have, true)) $alters[] = $ddl;
            }
            if ($alters !== []) DB::Exec("ALTER TABLE `{$t}` " . implode(', ', $alters));
        } catch (\Throwable $e) {
        }
    }

    /**
     * Add a notification.
     *
     * @param array $data Keys: message (required), title, icon, url,
     *                    userId (null=all), permissions (optional), expire (optional)
     * @return int|false Insert ID or false on failure.
     */
    public static function Add(array $data): int|false
    {
        self::ensureColumns();
        $table = self::getTableName();
        $type  = (string) ($data['type'] ?? 'info');
        if (!in_array($type, self::TYPES, true)) $type = 'info';
        $record = [
            'message'     => $data['message'] ?? '',
            'title'       => $data['title'] ?? null,
            // Fall back to the severity's default icon so every notification has one.
            'icon'        => $data['icon'] ?? self::typeIcon($type),
            'url'         => $data['url'] ?? ($data['link'] ?? null),
            'user_id'     => $data['userId'] ?? ($data['user_id'] ?? null),
            'type'        => $type,
            'permissions' => !empty($data['permissions']) ? json_encode($data['permissions']) : null,
            'expire_at'   => $data['expire'] ?? null,
            'is_read'     => 0,
            'created_at'  => date('Y-m-d H:i:s'),
        ];

        return DB::Insert($table, $record);
    }

    // ── Targeting helpers — one call sends to the right audience ──────────
    // Each accepts the same $data keys as Add() (message, title, icon, url,
    // type, expire), minus the recipient (which the helper sets).

    /** Notify a specific list of user ids. Returns how many rows were written. */
    public static function AddForUsers(array $userIds, array $data): int
    {
        $n = 0;
        foreach (array_unique(array_filter(array_map('intval', $userIds))) as $uid) {
            if (self::Add(['userId' => $uid] + $data) !== false) $n++;
        }
        return $n;
    }

    /** Broadcast to everyone (a single row with user_id = NULL). */
    public static function AddForEveryone(array $data): int|false
    {
        return self::Add(['userId' => null] + $data);
    }

    /** Notify every user whose `users.role` matches. */
    public static function AddForRole(string $role, array $data): int
    {
        try {
            $ids = array_column(DB::FetchAll("SELECT `id` FROM `users` WHERE `role` = ?", [$role]) ?: [], 'id');
        } catch (\Throwable $e) {
            $ids = [];
        }
        return self::AddForUsers($ids, $data);
    }

    /** Notify every member of a group (via the `user_groups` join table). */
    public static function AddForGroup(int $groupId, array $data): int
    {
        try {
            $ids = array_column(DB::FetchAll("SELECT `user_id` FROM `user_groups` WHERE `group_id` = ?", [$groupId]) ?: [], 'user_id');
        } catch (\Throwable $e) {
            $ids = [];
        }
        return self::AddForUsers($ids, $data);
    }

    /**
     * Notify the owner of a record: $row is the record, $ownerField is the column
     * that holds a user id (e.g. 'created_by', 'owner_id', 'assigned_to').
     */
    public static function AddForRecordOwner(array $row, string $ownerField, array $data): int|false
    {
        $uid = (int) ($row[$ownerField] ?? 0);
        return $uid > 0 ? self::Add(['userId' => $uid] + $data) : false;
    }

    /**
     * Get notifications for a user.
     *
     * @param int $userId
     * @param int $limit
     * @param bool $unreadOnly
     * @return array
     */
    public static function Get(int $userId, int $limit = 20, bool $unreadOnly = false): array
    {
        $table = self::getTableName();
        $where = "user_id IS NULL OR user_id = {$userId}";
        if ($unreadOnly) {
            $where .= " AND is_read = 0";
        }
        return DB::Select($table, $where, 'created_at DESC', "LIMIT {$limit}") ?: [];
    }

    /**
     * Mark a single notification as read.
     */
    public static function MarkRead(int $id): int|false
    {
        $table = self::getTableName();
        return DB::Update($table, ['is_read' => 1], ['id' => $id]);
    }

    /**
     * Mark all notifications as read for a user.
     */
    public static function MarkAllRead(int $userId): int|false
    {
        $table = self::getTableName();
        return DB::Update($table, ['is_read' => 1], "user_id = {$userId} OR user_id IS NULL");
    }

    /**
     * Count unread notifications for a user.
     */
    public static function Count(int $userId): int
    {
        $table = self::getTableName();
        $count = DB::FetchOne("SELECT COUNT(*) FROM `{$table}` WHERE (user_id IS NULL OR user_id = ?) AND is_read = 0", [$userId]);
        return (int) $count;
    }

    /**
     * Delete notifications older than $days days.
     */
    public static function DeleteOld(int $days = 30): int
    {
        $table = self::getTableName();
        $cutoff = date('Y-m-d H:i:s', strtotime("-{$days} days"));
        return DB::Delete($table, "created_at < '{$cutoff}'");
    }

    // ── Table name management ──────────────────────────────────────

    private static ?string $tableName = null;

    /**
     * Set the notification table name (called after Enable or loaded from config).
     */
    public static function SetTableName(string $tableName): void
    {
        self::$tableName = $tableName;
    }

    private static function getTableName(): string
    {
        if (self::$tableName) return self::$tableName;
        // Fallback: try to read from project state (if available)
        // For simplicity, we'll rely on Misc controller to set it.
        return 'notifications';
    }
}
