<?php
/**
 * CalloutPlaybooks — the standards library for unplanned callout response.
 *
 * A playbook packages: SLA response/resolution targets per severity (P1..P4) + a per-severity
 * escalation chain (which roles to notify, after how many hours).
 *
 * Playbooks are authored centrally (Admin/Executive) then attached to Services projects
 * (PC/PM). Only one playbook attached to a project at a time — reattachment soft-deletes
 * the previous config row so the audit trail stays intact.
 *
 * The actual callout events + response measurement land in a later slice; this ships
 * the library + attachment mechanic.
 */
class CalloutPlaybooks extends Model
{
    public const SEVERITIES = ['P1','P2','P3','P4'];

    /* ── Playbook header ────────────────────────────────────────────── */

    public function create(array $d, int $userId, string $userName): int
    {
        $code = trim((string)($d['code'] ?? ''));
        $name = trim((string)($d['name'] ?? ''));
        if ($code === '') throw new RuntimeException('Playbook code required.');
        if ($name === '') throw new RuntimeException('Playbook name required.');

        $db = new DB();
        $existing = $db->mySelect("SELECT COUNT(*) n FROM callout_playbooks WHERE code = :c AND deleted = 0", [':c' => $code]);
        if ((int)$existing[0]['n'] > 0) throw new RuntimeException('A playbook with code "' . $code . '" already exists.');

        $fb = $db->insertUpdate(
            "INSERT INTO callout_playbooks
                (code, name, description, is_active, created_by, created_by_name, created_on)
             VALUES (:c, :n, :d, 1, :u, :un, NOW())",
            [
                ':c' => $code, ':n' => $name, ':d' => (string)($d['description'] ?? ''),
                ':u' => $userId, ':un' => $userName,
            ]
        );
        return (int)($fb['last_id'] ?? 0);
    }

    public function findById(int $id): ?array
    {
        $db = new DB();
        $r = $db->mySelect("SELECT * FROM callout_playbooks WHERE c0d = :id AND deleted = 0", [':id' => $id]);
        return $r[0] ?? null;
    }

    public function listAll(bool $activeOnly = false): array
    {
        $db = new DB();
        $sql = "SELECT * FROM callout_playbooks WHERE deleted = 0";
        if ($activeOnly) $sql .= " AND is_active = 1";
        $sql .= " ORDER BY name";
        return $db->mySelect($sql, []) ?: [];
    }

    public function updateHeader(int $id, array $d, int $userId): void
    {
        $pb = $this->findById($id);
        if (!$pb) throw new RuntimeException('Playbook not found.');
        $db = new DB();
        $db->insertUpdate(
            "UPDATE callout_playbooks SET name = :n, description = :d, edited_by = :u, edited_on = NOW() WHERE c0d = :id",
            [
                ':n' => (string)($d['name'] ?? $pb['name']),
                ':d' => (string)($d['description'] ?? $pb['description']),
                ':u' => $userId, ':id' => $id,
            ]
        );
    }

    public function setActive(int $id, bool $active, int $userId): void
    {
        $db = new DB();
        $db->insertUpdate(
            "UPDATE callout_playbooks SET is_active = :a, edited_by = :u, edited_on = NOW() WHERE c0d = :id",
            [':a' => $active ? 1 : 0, ':u' => $userId, ':id' => $id]
        );
    }

    /* ── SLA rows (one per severity per playbook — upsert on duplicate) ── */

    public function upsertSla(int $playbookId, string $severity, float $responseHrs, float $resolutionHrs, string $notes = ''): int
    {
        $severity = strtoupper($severity);
        if (!in_array($severity, self::SEVERITIES, true)) throw new RuntimeException('Invalid severity: ' . $severity);
        if ($responseHrs < 0 || $resolutionHrs < 0)      throw new RuntimeException('Hours must be ≥ 0.');
        if ($resolutionHrs > 0 && $responseHrs > 0 && $responseHrs > $resolutionHrs) {
            throw new RuntimeException('Response target cannot exceed resolution target.');
        }

        $db = new DB();
        $existing = $db->mySelect(
            "SELECT c0d FROM callout_playbook_slas WHERE playbook_id = :p AND severity = :s AND deleted = 0",
            [':p' => $playbookId, ':s' => $severity]
        );
        if (!empty($existing)) {
            $db->insertUpdate(
                "UPDATE callout_playbook_slas SET response_target_hours = :r, resolution_target_hours = :rs, notes = :nt WHERE c0d = :id",
                [':r' => $responseHrs, ':rs' => $resolutionHrs, ':nt' => $notes, ':id' => (int)$existing[0]['c0d']]
            );
            return (int)$existing[0]['c0d'];
        }
        $fb = $db->insertUpdate(
            "INSERT INTO callout_playbook_slas (playbook_id, severity, response_target_hours, resolution_target_hours, notes, created_on)
             VALUES (:p, :s, :r, :rs, :nt, NOW())",
            [':p' => $playbookId, ':s' => $severity, ':r' => $responseHrs, ':rs' => $resolutionHrs, ':nt' => $notes]
        );
        return (int)($fb['last_id'] ?? 0);
    }

    public function slasFor(int $playbookId): array
    {
        $db = new DB();
        return $db->mySelect(
            "SELECT * FROM callout_playbook_slas WHERE playbook_id = :p AND deleted = 0 ORDER BY FIELD(severity,'P1','P2','P3','P4')",
            [':p' => $playbookId]
        ) ?: [];
    }

    public function deleteSla(int $slaId): void
    {
        $db = new DB();
        $db->insertUpdate("UPDATE callout_playbook_slas SET deleted = 1 WHERE c0d = :id", [':id' => $slaId]);
    }

    /* ── Escalation steps ───────────────────────────────────────────── */

    public function addEscalation(int $playbookId, string $severity, int $stepNo, string $role, float $notifyAfterHours, string $notes = ''): int
    {
        $severity = strtoupper($severity);
        if (!in_array($severity, self::SEVERITIES, true)) throw new RuntimeException('Invalid severity.');
        $role = trim($role);
        if ($role === '') throw new RuntimeException('Role required.');
        if ($notifyAfterHours < 0) throw new RuntimeException('notify_after_hours must be ≥ 0.');

        $db = new DB();
        $fb = $db->insertUpdate(
            "INSERT INTO callout_playbook_escalations (playbook_id, severity, step_no, role, notify_after_hours, notes, created_on)
             VALUES (:p, :s, :st, :r, :nh, :nt, NOW())",
            [':p' => $playbookId, ':s' => $severity, ':st' => $stepNo,
             ':r' => $role, ':nh' => $notifyAfterHours, ':nt' => $notes]
        );
        return (int)($fb['last_id'] ?? 0);
    }

    public function escalationsFor(int $playbookId, string $severity = ''): array
    {
        $db = new DB();
        $sql = "SELECT * FROM callout_playbook_escalations WHERE playbook_id = :p AND deleted = 0";
        $p = [':p' => $playbookId];
        if ($severity !== '') { $sql .= " AND severity = :s"; $p[':s'] = $severity; }
        $sql .= " ORDER BY FIELD(severity,'P1','P2','P3','P4'), step_no";
        return $db->mySelect($sql, $p) ?: [];
    }

    public function deleteEscalation(int $escalationId): void
    {
        $db = new DB();
        $db->insertUpdate("UPDATE callout_playbook_escalations SET deleted = 1 WHERE c0d = :id", [':id' => $escalationId]);
    }

    /* ── Project attachment ─────────────────────────────────────────── */

    /**
     * Attach a playbook to a project. Detaches any current active config (soft-delete) so
     * the audit trail keeps the history.
     */
    public function attachToProject(int $projectId, int $playbookId, int $userId, string $userName, string $notes = ''): int
    {
        $pb = $this->findById($playbookId);
        if (!$pb) throw new RuntimeException('Playbook not found.');
        if ((int)$pb['is_active'] !== 1) throw new RuntimeException('Playbook is inactive.');

        $db = new DB();
        // Detach any current active config
        $current = $this->activeForProject($projectId);
        if ($current && (int)$current['playbook_id'] === $playbookId) {
            // Already attached to this playbook — no-op.
            return (int)$current['c0d'];
        }
        if ($current) {
            $db->insertUpdate(
                "UPDATE project_callout_config
                    SET detached_on = NOW(), detached_by = :u, deleted = 1
                  WHERE c0d = :id",
                [':u' => $userId, ':id' => (int)$current['c0d']]
            );
        }
        $fb = $db->insertUpdate(
            "INSERT INTO project_callout_config
                (project_id, playbook_id, attached_on, attached_by, attached_by_name, notes)
             VALUES (:p, :pb, NOW(), :u, :un, :nt)",
            [':p' => $projectId, ':pb' => $playbookId, ':u' => $userId, ':un' => $userName, ':nt' => $notes]
        );
        return (int)($fb['last_id'] ?? 0);
    }

    /** Return the currently active (non-detached) config row for a project, if any. */
    public function activeForProject(int $projectId): ?array
    {
        $db = new DB();
        $r = $db->mySelect(
            "SELECT c.*, p.code AS playbook_code, p.name AS playbook_name
               FROM project_callout_config c
               JOIN callout_playbooks p ON p.c0d = c.playbook_id
              WHERE c.project_id = :p AND c.deleted = 0
              ORDER BY c.c0d DESC LIMIT 1",
            [':p' => $projectId]
        );
        return $r[0] ?? null;
    }

    /** Full attachment history for a project (audit — includes deleted/detached rows). */
    public function attachmentHistory(int $projectId): array
    {
        $db = new DB();
        return $db->mySelect(
            "SELECT c.*, p.code AS playbook_code, p.name AS playbook_name
               FROM project_callout_config c
               JOIN callout_playbooks p ON p.c0d = c.playbook_id
              WHERE c.project_id = :p
              ORDER BY c.c0d DESC",
            [':p' => $projectId]
        ) ?: [];
    }
}
