<?php
namespace App\Services;

use App\Core\Database;
use DateTime;
use RuntimeException;

/**
 * Central ticket business logic shared by the agent console, the customer
 * portal and the email-ingest cron job, so all three intake channels
 * converge on identical rules (BRD 5.1: "every channel converges on one
 * numbered record").
 */
class TicketService
{
    /** Status transition map — BRD 5.1 Figure 3. Status values are fixed (FR-SUP-016). */
    private const TRANSITIONS = [
        'NEW' => ['ASSIGNED', 'IN_PROGRESS', 'REJECTED'],
        'ASSIGNED' => ['IN_PROGRESS', 'PENDING_CUSTOMER', 'PENDING_THIRD_PARTY', 'ESCALATED', 'REJECTED'],
        'IN_PROGRESS' => ['PENDING_CUSTOMER', 'PENDING_THIRD_PARTY', 'ESCALATED', 'RESOLVED'],
        'PENDING_CUSTOMER' => ['IN_PROGRESS', 'ESCALATED'],
        'PENDING_THIRD_PARTY' => ['IN_PROGRESS', 'ESCALATED'],
        'ESCALATED' => ['IN_PROGRESS', 'PENDING_CUSTOMER', 'PENDING_THIRD_PARTY', 'RESOLVED'],
        'RESOLVED' => ['CLOSED', 'REOPENED'],
        'CLOSED' => ['REOPENED'],
        'REOPENED' => ['IN_PROGRESS', 'ASSIGNED'],
        'REJECTED' => [],
    ];

    public static function canTransition(string $from, string $to): bool
    {
        return in_array($to, self::TRANSITIONS[$from] ?? [], true);
    }

    public static function allowedTransitions(string $status): array
    {
        return self::TRANSITIONS[$status] ?? [];
    }

    /**
     * Create a ticket from any channel. $data keys: contact_id, product_id,
     * subject, description, category_id, sub_category_id, priority_id,
     * severity, channel, queue_id(optional).
     */
    public static function create(array $data, ?int $createdByUserId, ?int $createdByContactId): array
    {
        $contact = Database::one('SELECT * FROM contacts WHERE id = :id', ['id' => $data['contact_id']]);
        if (!$contact) throw new RuntimeException('Unknown requester.');

        $contract = Database::one(
            "SELECT c.* FROM contracts c
             JOIN contract_products cp ON cp.contract_id = c.id
             WHERE c.customer_id = :cust AND cp.product_id = :prod AND c.is_active = 1
             AND CURDATE() BETWEEN c.start_date AND c.end_date LIMIT 1",
            ['cust' => $contact['customer_id'], 'prod' => $data['product_id']]
        );
        $outOfEntitlement = $contract === null;

        $queueId = self::routeToQueue((int)$data['category_id'], (int)$data['product_id'], (int)$contact['customer_id']);
        $slaPolicy = SlaEngine::resolvePolicy($contract['tier'] ?? null, (int)$data['product_id']);

        $now = new DateTime();
        $responseDue = $resolutionDue = null;
        $firstResponseMinutes = $resolutionMinutes = null;
        if ($slaPolicy) {
            $target = SlaEngine::targetFor((int)$slaPolicy['id'], (int)$data['priority_id']);
            if ($target) {
                [$responseDue, $resolutionDue] = SlaEngine::computeDueDates($slaPolicy, $target, $now);
                $firstResponseMinutes = $target['first_response_minutes'];
                $resolutionMinutes = $target['resolution_minutes'];
            }
        }

        $ticketNo = TicketNumberGenerator::next();
        $status = $queueId ? 'ASSIGNED' : 'NEW';
        // "ASSIGNED" here means "routed to a queue" per BRD 5.1; an individual agent is assigned separately.
        if (!empty($data['assignee_id'])) {
            $status = 'ASSIGNED';
        }

        Database::beginTransaction();
        try {
            $ticketId = Database::insert('tickets', [
                'ticket_no' => $ticketNo,
                'customer_id' => $contact['customer_id'],
                'contact_id' => $contact['id'],
                'contract_id' => $contract['id'] ?? null,
                'product_id' => $data['product_id'],
                'version' => $data['version'] ?? null,
                'subject' => $data['subject'],
                'description' => $data['description'],
                'category_id' => $data['category_id'],
                'sub_category_id' => $data['sub_category_id'] ?? null,
                'priority_id' => $data['priority_id'],
                'severity' => $data['severity'] ?? 'Minor impact',
                'channel' => $data['channel'],
                'status' => $status,
                'queue_id' => $queueId,
                'assignee_id' => $data['assignee_id'] ?? null,
                'sla_policy_id' => $slaPolicy['id'] ?? null,
                'first_response_minutes' => $firstResponseMinutes,
                'resolution_minutes' => $resolutionMinutes,
                'response_due_at' => $responseDue?->format('Y-m-d H:i:s'),
                'resolution_due_at' => $resolutionDue?->format('Y-m-d H:i:s'),
                'is_out_of_entitlement' => $outOfEntitlement ? 1 : 0,
                'is_billable' => $outOfEntitlement ? 1 : 0,
                'created_by_user_id' => $createdByUserId,
                'created_by_contact_id' => $createdByContactId,
            ]);

            $initialMessageId = Database::insert('ticket_messages', [
                'ticket_id' => $ticketId,
                'user_id' => $createdByUserId,
                'contact_id' => $createdByUserId ? null : $createdByContactId,
                'type' => $data['channel'] === 'Email' ? 'inbound_email' : 'public_reply',
                'body' => $data['description'],
            ]);

            Database::insert('ticket_status_history', [
                'ticket_id' => $ticketId, 'from_status' => null, 'to_status' => $status,
                'changed_by_user_id' => $createdByUserId, 'changed_by_contact_id' => $createdByContactId,
            ]);

            self::audit($ticketId, $createdByUserId, 'created', null, null, $ticketNo);
            if ($outOfEntitlement) {
                self::audit($ticketId, $createdByUserId, 'flagged', 'entitlement', 'active', 'out_of_entitlement');
            }
            Database::commit();
        } catch (\Throwable $e) {
            Database::rollBack();
            throw $e;
        }

        $ticket = Database::one('SELECT * FROM tickets WHERE id = :id', ['id' => $ticketId]);
        NotificationService::send('ticket_created_ack', $ticket);
        // Not a real column — lets the caller attach creation-time uploads to the
        // opening message so they inherit its public visibility (see AttachmentService
        // callers in TicketController::store / PortalController::store).
        $ticket['initial_message_id'] = $initialMessageId;
        return $ticket;
    }

    /** FR-SUP-022: route a new ticket to a queue using configurable rules. */
    private static function routeToQueue(int $categoryId, int $productId, int $customerId): ?int
    {
        $rule = Database::one(
            "SELECT queue_id FROM queue_routing_rules
             WHERE is_active = 1 AND (category_id = :cat OR category_id IS NULL)
             AND (product_id = :prod OR product_id IS NULL)
             AND (customer_id = :cust OR customer_id IS NULL)
             ORDER BY priority_order ASC LIMIT 1",
            ['cat' => $categoryId, 'prod' => $productId, 'cust' => $customerId]
        );
        if ($rule) return (int)$rule['queue_id'];

        $fallback = Database::one("SELECT id FROM queues WHERE is_active = 1 ORDER BY id ASC LIMIT 1");
        return $fallback ? (int)$fallback['id'] : null;
    }

    /** @return int the new ticket_messages.id, so a caller can attach files to this specific reply. */
    public static function addMessage(array $ticket, string $type, string $body, ?int $userId, ?int $contactId, ?int $kbArticleId = null, bool $fromCanned = false): int
    {
        $ticketId = (int)$ticket['id'];

        // FR-SUP-034: resolve {{ticket_no}}, {{contact_name}}, {{agent_name}}, etc. in an
        // agent-authored reply or note — covers both a canned response inserted as-is and a
        // placeholder typed by hand. Customer-authored text (contact_id set) is left alone,
        // since a customer's own words are never a template.
        if ($userId && in_array($type, ['public_reply', 'internal_note'], true)) {
            $agentName = Database::scalar('SELECT name FROM users WHERE id = :id', ['id' => $userId]) ?: 'Support team';
            $body = NotificationService::fill($body, NotificationService::placeholdersFor($ticket, ['agent_name' => $agentName]));
        }

        $messageId = Database::insert('ticket_messages', [
            'ticket_id' => $ticketId, 'user_id' => $userId, 'contact_id' => $contactId,
            'type' => $type, 'body' => $body, 'kb_article_id' => $kbArticleId, 'is_from_canned' => $fromCanned ? 1 : 0,
        ]);

        if ($kbArticleId) {
            Database::insert('kb_article_usage', ['kb_article_id' => $kbArticleId, 'ticket_id' => $ticketId, 'action' => 'used_in_reply']);
        }

        if ($type === 'public_reply' && $userId) {
            if (!$ticket['first_response_at']) {
                $now = date('Y-m-d H:i:s');
                $breached = $ticket['response_due_at'] && strtotime($now) > strtotime($ticket['response_due_at']);
                Database::update('tickets', [
                    'first_response_at' => $now,
                    'is_response_breached' => $breached ? 1 : (int)$ticket['is_response_breached'],
                ], 'id = :id', ['id' => $ticketId]);
                Database::insert('sla_events', ['ticket_id' => $ticketId, 'event_type' => 'met', 'meta' => 'first response']);
            }
            self::audit($ticketId, $userId, 'public_reply_sent');
            $fresh = Database::one('SELECT * FROM tickets WHERE id = :id', ['id' => $ticketId]);
            NotificationService::send('ticket_public_reply', $fresh, ['message_body' => $body]);
        }

        if ($type === 'public_reply' && $contactId) {
            self::audit($ticketId, null, 'customer_replied');
            if (in_array($ticket['status'], ['PENDING_CUSTOMER', 'PENDING_THIRD_PARTY'], true)) {
                self::changeStatus($ticket, 'IN_PROGRESS', null, 'Customer replied — clock resumed');
            }
            NotificationService::send('ticket_customer_reply', $ticket);
        }

        if ($type === 'internal_note' && $userId) {
            self::audit($ticketId, $userId, 'internal_note_added');
        }

        return $messageId;
    }

    /**
     * @param array $extra resolution_code, root_cause, reason, justification
     */
    public static function changeStatus(array $ticket, string $newStatus, ?int $userId, ?string $reason = null, array $extra = []): array
    {
        $ticketId = (int)$ticket['id'];
        $from = $ticket['status'];

        if ($from === $newStatus) return $ticket;
        if (!self::canTransition($from, $newStatus)) {
            throw new RuntimeException("Cannot move a ticket from {$from} to {$newStatus}.");
        }
        if ($newStatus === 'RESOLVED' && (empty($extra['resolution_code']) || empty($extra['root_cause']))) {
            throw new RuntimeException('A resolution code and root cause are required before a ticket can be resolved.');
        }
        if ($newStatus === 'REJECTED' && empty($reason)) {
            throw new RuntimeException('A reason is required to reject a ticket.');
        }

        $update = ['status' => $newStatus];

        if (in_array($newStatus, ['PENDING_CUSTOMER', 'PENDING_THIRD_PARTY'], true)) {
            SlaEngine::pause($ticketId);
        }
        if (in_array($from, ['PENDING_CUSTOMER', 'PENDING_THIRD_PARTY'], true) && !in_array($newStatus, ['PENDING_CUSTOMER', 'PENDING_THIRD_PARTY'], true)) {
            SlaEngine::resume($ticketId);
        }

        if ($newStatus === 'RESOLVED') {
            $update['resolved_at'] = date('Y-m-d H:i:s');
            $update['resolution_code'] = $extra['resolution_code'];
            $update['root_cause'] = $extra['root_cause'];
        }
        if ($newStatus === 'CLOSED') {
            $update['closed_at'] = date('Y-m-d H:i:s');
            $update['csat_requested'] = 1;
        }
        if ($newStatus === 'REOPENED') {
            $update['resolved_at'] = null;
            $update['closed_at'] = null;
        }
        if ($newStatus === 'REJECTED') {
            $update['rejected_reason'] = $reason;
        }
        if (!empty($extra['justification'])) {
            $update['breach_justification'] = $extra['justification'];
        }

        Database::update('tickets', $update, 'id = :id', ['id' => $ticketId]);
        Database::insert('ticket_status_history', [
            'ticket_id' => $ticketId, 'from_status' => $from, 'to_status' => $newStatus,
            'changed_by_user_id' => $userId, 'reason' => $reason,
        ]);
        self::audit($ticketId, $userId, 'status_changed', 'status', $from, $newStatus);

        $fresh = Database::one('SELECT * FROM tickets WHERE id = :id', ['id' => $ticketId]);

        if ($newStatus === 'CLOSED') {
            NotificationService::send('ticket_closed', $fresh);
        } elseif (in_array($newStatus, ['RESOLVED', 'REJECTED', 'REOPENED'], true)) {
            NotificationService::send('ticket_status_change', $fresh);
        }

        return $fresh;
    }

    public static function assign(array $ticket, int $assigneeId, ?int $userId, ?string $reason = null): array
    {
        $ticketId = (int)$ticket['id'];
        $previous = $ticket['assignee_id'];
        $update = ['assignee_id' => $assigneeId];
        if ($ticket['status'] === 'NEW') $update['status'] = 'ASSIGNED';
        if ($previous && (int)$previous !== $assigneeId) {
            $update['reassignment_count'] = (int)$ticket['reassignment_count'] + 1;
        }
        Database::update('tickets', $update, 'id = :id', ['id' => $ticketId]);
        self::audit($ticketId, $userId, 'assigned', 'assignee_id', (string)$previous, (string)$assigneeId, $reason);

        $fresh = Database::one('SELECT * FROM tickets WHERE id = :id', ['id' => $ticketId]);
        NotificationService::send('ticket_assigned', $fresh);
        return $fresh;
    }

    public static function escalate(array $ticket, ?int $userId, string $reason): array
    {
        $ticketId = (int)$ticket['id'];
        $level = (int)$ticket['escalation_level'] + 1;
        Database::update('tickets', ['escalation_level' => $level, 'status' => 'ESCALATED'], 'id = :id', ['id' => $ticketId]);
        Database::insert('escalation_events', [
            'ticket_id' => $ticketId, 'level' => $level, 'notified' => 'LEAD,HEAD', 'reason' => $reason, 'created_by_user_id' => $userId,
        ]);
        self::audit($ticketId, $userId, 'escalated', 'escalation_level', (string)($level - 1), (string)$level, $reason);

        $fresh = Database::one('SELECT * FROM tickets WHERE id = :id', ['id' => $ticketId]);
        NotificationService::send('ticket_escalated', $fresh, ['level' => $level]);
        return $fresh;
    }

    public static function updateClassification(array $ticket, array $fields, ?int $userId, string $reason): array
    {
        $ticketId = (int)$ticket['id'];
        $trackable = ['priority_id', 'category_id', 'sub_category_id', 'severity'];
        $update = [];
        foreach ($trackable as $f) {
            if (isset($fields[$f]) && (string)$fields[$f] !== (string)$ticket[$f]) {
                self::audit($ticketId, $userId, 'classification_changed', $f, (string)$ticket[$f], (string)$fields[$f], $reason);
                $update[$f] = $fields[$f];
            }
        }
        if (empty($update)) return $ticket;

        if (isset($update['priority_id']) && $ticket['sla_policy_id']) {
            // FR-SUP-051: recompute SLA targets from the original creation time when priority changes.
            $policy = Database::one('SELECT * FROM sla_policies WHERE id = :id', ['id' => $ticket['sla_policy_id']]);
            $target = SlaEngine::targetFor((int)$ticket['sla_policy_id'], (int)$update['priority_id']);
            if ($policy && $target) {
                [$respDue, $resDue] = SlaEngine::computeDueDates($policy, $target, new DateTime($ticket['created_at']));
                $update['response_due_at'] = $ticket['first_response_at'] ? $ticket['response_due_at'] : $respDue->format('Y-m-d H:i:s');
                $update['resolution_due_at'] = $resDue->format('Y-m-d H:i:s');
                $update['first_response_minutes'] = $target['first_response_minutes'];
                $update['resolution_minutes'] = $target['resolution_minutes'];
                self::audit($ticketId, $userId, 'sla_recalculated', 'resolution_due_at', $ticket['resolution_due_at'], $resDue->format('Y-m-d H:i:s'));
            }
        }

        Database::update('tickets', $update, 'id = :id', ['id' => $ticketId]);
        return Database::one('SELECT * FROM tickets WHERE id = :id', ['id' => $ticketId]);
    }

    public static function logTime(int $ticketId, int $userId, string $logDate, int $durationMinutes, string $activityType, ?string $description, bool $isBillable, ?string $billableReason): void
    {
        Database::insert('time_logs', [
            'ticket_id' => $ticketId, 'user_id' => $userId, 'log_date' => $logDate,
            'duration_minutes' => $durationMinutes, 'activity_type' => $activityType,
            'description' => $description, 'is_billable' => $isBillable ? 1 : 0, 'billable_reason' => $billableReason,
        ]);
        self::audit($ticketId, $userId, 'time_logged', null, null, $durationMinutes . ' min');

        $ticket = Database::one('SELECT * FROM tickets WHERE id = :id', ['id' => $ticketId]);
        if ($isBillable === false && $ticket['contract_id']) {
            $hours = $durationMinutes / 60;
            Database::update('contracts', [
                'support_hours_used' => (float)Database::scalar('SELECT support_hours_used FROM contracts WHERE id = :id', ['id' => $ticket['contract_id']]) + $hours,
            ], 'id = :id', ['id' => $ticket['contract_id']]);

            $contract = Database::one('SELECT * FROM contracts WHERE id = :id', ['id' => $ticket['contract_id']]);
            if ($contract && $contract['support_hours_total'] > 0) {
                $pct = ($contract['support_hours_used'] / $contract['support_hours_total']) * 100;
                if ($pct >= 80) {
                    NotificationService::send('sla_warning', $ticket, ['pct' => round($pct)]);
                }
            }
        }
    }

    public static function audit(int $ticketId, ?int $userId, string $action, ?string $field = null, ?string $old = null, ?string $new = null, ?string $extraNote = null): void
    {
        Database::insert('ticket_audit_log', [
            'ticket_id' => $ticketId, 'user_id' => $userId, 'action' => $action,
            'field_name' => $field, 'old_value' => $old, 'new_value' => $extraNote ? "{$new} ({$extraNote})" : $new,
        ]);
    }
}
