<?php
namespace App\Services;

use App\Core\Database;
use DateTime;
use DateInterval;

/**
 * Computes SLA due dates against a business calendar, and evaluates the
 * warning/escalation state of open tickets. Implements BRD 5.2 / 6.5:
 * FR-SUP-042 to FR-SUP-051.
 *
 * Two rules matter and are deliberately explicit rather than folded into
 * a generic date-math library, because a customer disputing a breach must
 * be able to be shown exactly how the elapsed time was calculated:
 *   1. The clock only advances during the calendar's working hours,
 *      unless the policy is 24x7.
 *   2. PENDING_CUSTOMER / PENDING_THIRD_PARTY pause the clock; the paused
 *      seconds are added back on top of the due date when resuming.
 */
class SlaEngine
{
    /**
     * Add $minutes of business time to $from, according to the calendar.
     * Returns a DateTime for the resulting due date.
     */
    public static function addBusinessMinutes(DateTime $from, int $minutes, array $calendar, bool $is24x7): DateTime
    {
        if ($is24x7) {
            $due = clone $from;
            $due->add(new DateInterval('PT' . $minutes . 'M'));
            return $due;
        }

        $workDays = array_map('intval', explode(',', $calendar['work_days'])); // ISO 1..7
        $holidays = self::holidaySet((int)$calendar['id']);
        [$startH, $startM] = array_map('intval', explode(':', $calendar['start_time']));
        [$endH, $endM] = array_map('intval', explode(':', $calendar['end_time']));

        $cursor = clone $from;
        $remaining = $minutes;

        // Snap the cursor forward to the next working moment if it starts outside hours.
        $cursor = self::snapToWorkingMoment($cursor, $workDays, $holidays, $startH, $startM, $endH, $endM);

        while ($remaining > 0) {
            $dayEnd = (clone $cursor)->setTime($endH, $endM, 0);
            $minutesLeftToday = (int)(($dayEnd->getTimestamp() - $cursor->getTimestamp()) / 60);

            if ($minutesLeftToday <= 0) {
                $cursor = self::nextWorkingDayStart($cursor, $workDays, $holidays, $startH, $startM);
                continue;
            }

            $consume = min($remaining, $minutesLeftToday);
            $cursor->add(new DateInterval('PT' . $consume . 'M'));
            $remaining -= $consume;

            if ($remaining > 0) {
                $cursor = self::nextWorkingDayStart($cursor, $workDays, $holidays, $startH, $startM);
            }
        }

        return $cursor;
    }

    private static function snapToWorkingMoment(DateTime $dt, array $workDays, array $holidays, int $sh, int $sm, int $eh, int $em): DateTime
    {
        $dt = clone $dt;
        while (true) {
            $iso = (int)$dt->format('N');
            $dateStr = $dt->format('Y-m-d');
            if (!in_array($iso, $workDays, true) || in_array($dateStr, $holidays, true)) {
                $dt->modify('+1 day')->setTime($sh, $sm, 0);
                continue;
            }
            $dayStart = (clone $dt)->setTime($sh, $sm, 0);
            $dayEnd = (clone $dt)->setTime($eh, $em, 0);
            if ($dt < $dayStart) { return $dayStart; }
            if ($dt >= $dayEnd) { $dt->modify('+1 day')->setTime($sh, $sm, 0); continue; }
            return $dt;
        }
    }

    private static function nextWorkingDayStart(DateTime $dt, array $workDays, array $holidays, int $sh, int $sm): DateTime
    {
        $dt = (clone $dt)->modify('+1 day')->setTime($sh, $sm, 0);
        while (true) {
            $iso = (int)$dt->format('N');
            $dateStr = $dt->format('Y-m-d');
            if (!in_array($iso, $workDays, true) || in_array($dateStr, $holidays, true)) {
                $dt->modify('+1 day')->setTime($sh, $sm, 0);
                continue;
            }
            return $dt;
        }
    }

    private static function holidaySet(int $calendarId): array
    {
        $rows = Database::all('SELECT holiday_date FROM holidays WHERE calendar_id = :c', ['c' => $calendarId]);
        return array_map(fn($r) => $r['holiday_date'], $rows);
    }

    /** Resolve the SLA policy that applies to a contract tier (+ optional product). */
    public static function resolvePolicy(?string $contractTier, ?int $productId): ?array
    {
        $tier = $contractTier ?: 'Standard';
        $policy = Database::one(
            "SELECT * FROM sla_policies WHERE contract_tier = :tier AND is_active = 1
             AND effective_from <= CURDATE() ORDER BY effective_from DESC, version DESC LIMIT 1",
            ['tier' => $tier]
        );
        return $policy ?: null;
    }

    public static function targetFor(int $policyId, int $priorityId): ?array
    {
        return Database::one('SELECT * FROM sla_targets WHERE sla_policy_id = :p AND priority_id = :pr', [
            'p' => $policyId, 'pr' => $priorityId,
        ]);
    }

    /** Compute response/resolution due dates for a brand-new ticket. */
    public static function computeDueDates(array $policy, array $target, DateTime $createdAt): array
    {
        $calendar = Database::one('SELECT * FROM business_calendars WHERE id = :id', ['id' => $policy['calendar_id']]);
        $responseDue = self::addBusinessMinutes($createdAt, (int)$target['first_response_minutes'], $calendar, (bool)$policy['is_24x7']);
        $resolutionDue = self::addBusinessMinutes($createdAt, (int)$target['resolution_minutes'], $calendar, (bool)$policy['is_24x7']);
        return [$responseDue, $resolutionDue];
    }

    /**
     * Evaluate every open ticket: raise warnings, flag breaches, and escalate
     * per the policy's escalation matrix. Intended to be run from cron every
     * few minutes (NFR-04). Returns a summary array for logging.
     */
    public static function evaluateAll(): array
    {
        $summary = ['warned' => 0, 'breached' => 0, 'escalated' => 0];

        $tickets = Database::all(
            "SELECT * FROM tickets
             WHERE status NOT IN ('RESOLVED','CLOSED','REJECTED')
             AND resolution_due_at IS NOT NULL"
        );

        foreach ($tickets as $ticket) {
            self::evaluateTicket($ticket, $summary);
        }

        return $summary;
    }

    private static function evaluateTicket(array $ticket, array &$summary): void
    {
        $now = new DateTime();
        $ticketId = (int)$ticket['id'];

        if (in_array($ticket['status'], ['PENDING_CUSTOMER', 'PENDING_THIRD_PARTY'], true)) {
            return; // clock paused — nothing to evaluate right now
        }

        // First-response breach
        if (!$ticket['first_response_at'] && !$ticket['is_response_breached'] && $ticket['response_due_at']
            && strtotime($ticket['response_due_at']) < time()) {
            Database::update('tickets', ['is_response_breached' => 1], 'id = :id', ['id' => $ticketId]);
            self::logEvent($ticketId, 'response_breach');
            NotificationService::send('sla_breach', $ticket, ['pct' => 100]);
            $summary['breached']++;
        }

        if (!$ticket['sla_policy_id'] || !$ticket['resolution_due_at']) return;

        $createdAt = new DateTime($ticket['created_at']);
        $dueAt = new DateTime($ticket['resolution_due_at']);
        $totalSeconds = max(1, $dueAt->getTimestamp() - $createdAt->getTimestamp());
        $elapsedSeconds = max(0, $now->getTimestamp() - $createdAt->getTimestamp() - (int)$ticket['sla_paused_seconds']);
        $pctElapsed = min(999, (int)round(($elapsedSeconds / $totalSeconds) * 100));

        $target = Database::one('SELECT * FROM sla_targets WHERE sla_policy_id = :p AND priority_id = :pr', [
            'p' => $ticket['sla_policy_id'], 'pr' => $ticket['priority_id'],
        ]);
        $warn1 = $target['warn_pct_1'] ?? 50;
        $warn2 = $target['warn_pct_2'] ?? 80;

        $alreadyWarned50 = (bool)Database::scalar("SELECT COUNT(*) FROM sla_events WHERE ticket_id = :t AND event_type = 'warn_50'", ['t' => $ticketId]);
        $alreadyWarned80 = (bool)Database::scalar("SELECT COUNT(*) FROM sla_events WHERE ticket_id = :t AND event_type = 'warn_80'", ['t' => $ticketId]);

        if ($pctElapsed >= $warn1 && !$alreadyWarned50 && $pctElapsed < 100) {
            self::logEvent($ticketId, 'warn_50', "{$pctElapsed}%");
            NotificationService::send('sla_warning', $ticket, ['pct' => $pctElapsed]);
            self::maybeEscalate($ticket, $pctElapsed, false);
            $summary['warned']++;
        }
        if ($pctElapsed >= $warn2 && !$alreadyWarned80 && $pctElapsed < 100) {
            self::logEvent($ticketId, 'warn_80', "{$pctElapsed}%");
            NotificationService::send('sla_warning', $ticket, ['pct' => $pctElapsed]);
            self::maybeEscalate($ticket, $pctElapsed, false);
            $summary['warned']++;
        }

        if (!$ticket['is_resolution_breached'] && $pctElapsed >= 100) {
            Database::update('tickets', ['is_resolution_breached' => 1, 'status' => $ticket['status'] === 'NEW' ? 'ASSIGNED' : $ticket['status']], 'id = :id', ['id' => $ticketId]);
            self::logEvent($ticketId, 'resolution_breach');
            NotificationService::send('sla_breach', $ticket, ['pct' => $pctElapsed]);
            self::maybeEscalate($ticket, $pctElapsed, true);
            $summary['breached']++;
        }
    }

    private static function maybeEscalate(array $ticket, int $pctElapsed, bool $breached): void
    {
        if (!$ticket['sla_policy_id']) return;
        $levels = Database::all('SELECT * FROM escalation_levels WHERE sla_policy_id = :p ORDER BY level ASC', ['p' => $ticket['sla_policy_id']]);
        foreach ($levels as $level) {
            $matches = $breached ? (bool)$level['trigger_on_breach'] : ($level['trigger_pct'] !== null && $pctElapsed >= (int)$level['trigger_pct']);
            if (!$matches) continue;
            if ((int)$ticket['escalation_level'] >= (int)$level['level']) continue;

            Database::update('tickets', [
                'escalation_level' => $level['level'],
                'status' => 'ESCALATED',
            ], 'id = :id', ['id' => $ticket['id']]);

            Database::insert('escalation_events', [
                'ticket_id' => $ticket['id'],
                'level' => $level['level'],
                'notified' => $level['notify_role'],
                'reason' => $level['action_note'],
            ]);
            self::logEvent((int)$ticket['id'], 'escalate', 'Level ' . $level['level']);
            NotificationService::send('ticket_escalated', $ticket, ['level' => $level['level']]);
        }
    }

    private static function logEvent(int $ticketId, string $type, ?string $meta = null): void
    {
        Database::insert('sla_events', ['ticket_id' => $ticketId, 'event_type' => $type, 'meta' => $meta]);
    }

    /** Pause the clock (entering PENDING_CUSTOMER / PENDING_THIRD_PARTY). */
    public static function pause(int $ticketId): void
    {
        Database::update('tickets', ['paused_at' => date('Y-m-d H:i:s')], 'id = :id', ['id' => $ticketId]);
        self::logEvent($ticketId, 'pause');
    }

    /** Resume the clock and push due dates out by however long it was paused. */
    public static function resume(int $ticketId): void
    {
        $ticket = Database::one('SELECT * FROM tickets WHERE id = :id', ['id' => $ticketId]);
        if (!$ticket || !$ticket['paused_at']) return;

        $pausedSeconds = time() - strtotime($ticket['paused_at']);
        $newPausedTotal = (int)$ticket['sla_paused_seconds'] + max(0, $pausedSeconds);

        $update = ['sla_paused_seconds' => $newPausedTotal, 'paused_at' => null];
        if ($ticket['response_due_at'] && !$ticket['first_response_at']) {
            $update['response_due_at'] = date('Y-m-d H:i:s', strtotime($ticket['response_due_at']) + $pausedSeconds);
        }
        if ($ticket['resolution_due_at']) {
            $update['resolution_due_at'] = date('Y-m-d H:i:s', strtotime($ticket['resolution_due_at']) + $pausedSeconds);
        }
        Database::update('tickets', $update, 'id = :id', ['id' => $ticketId]);
        self::logEvent($ticketId, 'resume', fmt_minutes((int)round($pausedSeconds / 60)) . ' paused');
    }
}
