<?php

namespace MintyPHP\Module\Helpdesk\Service;

/**
 * Rule-template based recommendation engine for support dashboard actions.
 */
class SystemRecommendationEngine
{
    /** @var array<int, string> */
    private const CLOSED_TICKET_STATES = ['Erledigt', 'Resolved', 'Closed', 'Geschlossen'];

    /**
     * @param array<int, array<string, mixed>> $tickets
     * @param array<string, mixed>|null $escalationHealth
     * @param array<string, mixed> $config
     * @return array{
     *   recommendations: array<int, array{
     *     recommendation_id: string,
     *     rule_id: string,
     *     ticket_no: string,
     *     message_key: string,
     *     message_params: array<string, string|int|float|bool>,
     *     priority: int,
     *     urgency_score: int,
     *     age_hours: int|null,
     *     source: string
     *   }>,
     *   meta: array{
     *     applied_rules: array<int, string>,
     *     max_items: int,
     *     escalation_data_available: bool
     *   }
     * }
     */
    public function buildRecommendations(array $tickets, ?array $escalationHealth, array $config): array
    {
        $maxItems = max(1, min(20, (int) ($config['max_items'] ?? 5)));
        $rules = is_array($config['rules'] ?? null) ? $config['rules'] : [];

        $openTickets = $this->normalizeOpenTickets($tickets);
        $openTicketCount = count($openTickets);

        $escalationAvailable = is_array($escalationHealth) && (($escalationHealth['ok'] ?? false) === true);
        $escalationOverdueEntries = $escalationAvailable && is_array($escalationHealth['entries'] ?? null)
            ? $escalationHealth['entries']
            : [];
        $escalationMatches = $escalationAvailable && is_array($escalationHealth['matches'] ?? null)
            ? $escalationHealth['matches']
            : [];
        $escalationByTicket = $this->buildEscalationByTicketMap($escalationMatches);

        /** @var array<int, array{
         *   recommendation_id: string,
         *   rule_id: string,
         *   ticket_no: string,
         *   message_key: string,
         *   message_params: array<string, string|int|float|bool>,
         *   priority: int,
         *   urgency_score: int,
         *   age_hours: int|null,
         *   source: string
         * }> $candidates
         */
        $candidates = [];
        /** @var array<string, true> $appliedRulesMap */
        $appliedRulesMap = [];

        // Rule 1: escalation_overdue
        $rule = $this->getRuleConfig($rules, 'escalation_overdue');
        if ($rule['enabled'] && $escalationAvailable) {
            $appliedRulesMap['escalation_overdue'] = true;
            $minOverdueMinutes = max(0, (int) ($rule['min_overdue_minutes'] ?? 0));
            $minOverdueSeconds = $minOverdueMinutes * 60;

            foreach ($escalationOverdueEntries as $entry) {
                if (!is_array($entry)) {
                    continue;
                }

                $ticketNo = trim((string) ($entry['ticket_no'] ?? ''));
                if ($ticketNo === '') {
                    continue;
                }

                $overdueSeconds = max(0, (int) ($entry['overdue_seconds'] ?? 0));
                if ($overdueSeconds < $minOverdueSeconds) {
                    continue;
                }

                $ageHours = $openTickets[$ticketNo]['age_hours'] ?? null;
                $overdueHours = max(1, (int) ceil($overdueSeconds / 3600));
                $escalationCode = trim((string) ($entry['escalation_code'] ?? ''));

                $candidates[] = $this->createCandidate(
                    'escalation_overdue',
                    $ticketNo,
                    'helpdesk.recommendation.escalation_overdue',
                    [
                        'overdue_hours' => $overdueHours,
                        'escalation_code' => $escalationCode,
                    ],
                    (int) $rule['priority'],
                    $overdueSeconds,
                    $ageHours,
                    'escalation'
                );
            }
        }

        // Rule 2: high_risk_aging
        $rule = $this->getRuleConfig($rules, 'high_risk_aging');
        if ($rule['enabled'] && $escalationAvailable) {
            $appliedRulesMap['high_risk_aging'] = true;
            $minAgeHours = max(0, (int) ($rule['min_age_hours'] ?? 8));
            $codes = $this->normalizeCodes($rule['codes'] ?? []);
            $codeMap = array_fill_keys($codes, true);

            foreach ($openTickets as $ticketNo => $ticket) {
                $ageHours = $ticket['age_hours'];
                if (!is_int($ageHours) || $ageHours < $minAgeHours) {
                    continue;
                }

                $match = $escalationByTicket[$ticketNo] ?? null;
                if (!is_array($match)) {
                    continue;
                }

                $escalationCode = strtoupper(trim($match['escalation_code']));
                if ($escalationCode === '' || !isset($codeMap[$escalationCode])) {
                    continue;
                }

                $candidates[] = $this->createCandidate(
                    'high_risk_aging',
                    $ticketNo,
                    'helpdesk.recommendation.high_risk_aging',
                    [
                        'age_hours' => $ageHours,
                        'escalation_code' => $escalationCode,
                    ],
                    (int) $rule['priority'],
                    $ageHours,
                    $ageHours,
                    'tickets'
                );
            }
        }

        // Rule 3: unassigned_open
        $rule = $this->getRuleConfig($rules, 'unassigned_open');
        if ($rule['enabled']) {
            $appliedRulesMap['unassigned_open'] = true;
            $minAgeHours = max(0, (int) ($rule['min_age_hours'] ?? 2));

            foreach ($openTickets as $ticketNo => $ticket) {
                $ageHours = $ticket['age_hours'];
                if (!is_int($ageHours) || $ageHours < $minAgeHours) {
                    continue;
                }

                if ($ticket['support_user_name'] !== '') {
                    continue;
                }

                $candidates[] = $this->createCandidate(
                    'unassigned_open',
                    $ticketNo,
                    'helpdesk.recommendation.unassigned_open',
                    ['age_hours' => $ageHours],
                    (int) $rule['priority'],
                    $ageHours,
                    $ageHours,
                    'tickets'
                );
            }
        }

        // Rule 4: stale_open
        $rule = $this->getRuleConfig($rules, 'stale_open');
        if ($rule['enabled']) {
            $appliedRulesMap['stale_open'] = true;
            $minAgeHours = max(0, (int) ($rule['min_age_hours'] ?? 48));

            foreach ($openTickets as $ticketNo => $ticket) {
                $ageHours = $ticket['age_hours'];
                if (!is_int($ageHours) || $ageHours < $minAgeHours) {
                    continue;
                }

                $candidates[] = $this->createCandidate(
                    'stale_open',
                    $ticketNo,
                    'helpdesk.recommendation.stale_open',
                    ['age_hours' => $ageHours],
                    (int) $rule['priority'],
                    $ageHours,
                    $ageHours,
                    'tickets'
                );
            }
        }

        // Rule 5: customer_backlog
        $rule = $this->getRuleConfig($rules, 'customer_backlog');
        if ($rule['enabled']) {
            $appliedRulesMap['customer_backlog'] = true;
            $minOpenTickets = max(1, (int) ($rule['min_open_tickets'] ?? 6));
            if ($openTicketCount >= $minOpenTickets) {
                $oldest = $this->findOldestOpenTicket($openTickets);
                if (is_array($oldest)) {
                    $ageHours = $oldest['age_hours'];
                    $urgencyScore = ($openTicketCount * 1000) + (is_int($ageHours) ? $ageHours : 0);
                    $candidates[] = $this->createCandidate(
                        'customer_backlog',
                        (string) $oldest['ticket_no'],
                        'helpdesk.recommendation.customer_backlog',
                        ['open_tickets' => $openTicketCount],
                        (int) $rule['priority'],
                        $urgencyScore,
                        is_int($ageHours) ? $ageHours : null,
                        'tickets'
                    );
                }
            }
        }

        $deduped = $this->dedupeByRuleAndTicket($candidates);
        usort($deduped, static function (array $a, array $b): int {
            $priorityCmp = (int) $b['priority'] <=> (int) $a['priority'];
            if ($priorityCmp !== 0) {
                return $priorityCmp;
            }

            $urgencyCmp = (int) $b['urgency_score'] <=> (int) $a['urgency_score'];
            if ($urgencyCmp !== 0) {
                return $urgencyCmp;
            }

            return strcmp((string) $a['ticket_no'], (string) $b['ticket_no']);
        });

        return [
            'recommendations' => array_slice($deduped, 0, $maxItems),
            'meta' => [
                'applied_rules' => array_keys($appliedRulesMap),
                'max_items' => $maxItems,
                'escalation_data_available' => $escalationAvailable,
            ],
        ];
    }

    /**
     * @param array<string, mixed> $rules
     * @return array<string, mixed>
     */
    private function getRuleConfig(array $rules, string $ruleId): array
    {
        $rule = $rules[$ruleId] ?? [];
        if (!is_array($rule)) {
            $rule = [];
        }

        return $rule;
    }

    /**
     * @param array<int, array<string, mixed>> $matches
     * @return array<string, array{escalation_code: string, age_hours: int|null}>
     */
    private function buildEscalationByTicketMap(array $matches): array
    {
        $map = [];
        foreach ($matches as $match) {
            $ticketNo = trim((string) ($match['ticket_no'] ?? ''));
            if ($ticketNo === '') {
                continue;
            }

            $map[$ticketNo] = [
                'escalation_code' => trim((string) ($match['escalation_code'] ?? '')),
                'age_hours' => is_numeric($match['age_hours'] ?? null) ? (int) $match['age_hours'] : null,
            ];
        }

        return $map;
    }

    /**
     * @param array<int, string>|string $codesValue
     * @return array<int, string>
     */
    private function normalizeCodes(array|string $codesValue): array
    {
        if (is_string($codesValue)) {
            $codesValue = explode(',', $codesValue);
        }

        $normalized = [];
        foreach ($codesValue as $code) {
            $value = strtoupper(trim((string) $code));
            if ($value === '') {
                continue;
            }

            $normalized[] = $value;
        }

        return array_values(array_unique($normalized));
    }

    /**
     * @param array<string, array{ticket_no: string, support_user_name: string, age_hours: int|null}> $openTickets
     * @return array{ticket_no: string, support_user_name: string, age_hours: int|null}|null
     */
    private function findOldestOpenTicket(array $openTickets): ?array
    {
        $oldest = null;
        $oldestAge = -1;

        foreach ($openTickets as $ticket) {
            $ageHours = is_int($ticket['age_hours']) ? $ticket['age_hours'] : -1;
            if ($oldest === null || $ageHours > $oldestAge) {
                $oldest = $ticket;
                $oldestAge = $ageHours;
            }
        }

        return $oldest;
    }

    /**
     * @param array<int, array{
     *   recommendation_id: string,
     *   rule_id: string,
     *   ticket_no: string,
     *   message_key: string,
     *   message_params: array<string, string|int|float|bool>,
     *   priority: int,
     *   urgency_score: int,
     *   age_hours: int|null,
     *   source: string
     * }> $candidates
     * @return array<int, array{
     *   recommendation_id: string,
     *   rule_id: string,
     *   ticket_no: string,
     *   message_key: string,
     *   message_params: array<string, string|int|float|bool>,
     *   priority: int,
     *   urgency_score: int,
     *   age_hours: int|null,
     *   source: string
     * }>
     */
    private function dedupeByRuleAndTicket(array $candidates): array
    {
        $unique = [];
        foreach ($candidates as $candidate) {
            $key = (string) $candidate['rule_id'] . '|' . (string) $candidate['ticket_no'];
            if (isset($unique[$key])) {
                continue;
            }

            $unique[$key] = $candidate;
        }

        return array_values($unique);
    }

    /**
     * @param array<int, array<string, mixed>> $tickets
     * @return array<string, array{ticket_no: string, support_user_name: string, age_hours: int|null}>
     */
    private function normalizeOpenTickets(array $tickets): array
    {
        $nowTs = time();
        $openTickets = [];

        foreach ($tickets as $ticket) {

            $state = trim((string) ($ticket['Ticket_State'] ?? ''));
            if (in_array($state, self::CLOSED_TICKET_STATES, true)) {
                continue;
            }

            $ticketNo = trim((string) ($ticket['No'] ?? ''));
            if ($ticketNo === '') {
                continue;
            }

            $activityTs = $this->resolveActivityTimestamp($ticket);
            $ageHours = null;
            if ($activityTs !== null) {
                $ageHours = max(0, intdiv(max(0, $nowTs - $activityTs), 3600));
            }

            $openTickets[$ticketNo] = [
                'ticket_no' => $ticketNo,
                'support_user_name' => trim((string) ($ticket['Support_User_Name'] ?? '')),
                'age_hours' => $ageHours,
            ];
        }

        return $openTickets;
    }

    /**
     * @param array<string, mixed> $ticket
     */
    private function resolveActivityTimestamp(array $ticket): ?int
    {
        $lastActivity = trim((string) ($ticket['Last_Activity_Date'] ?? ''));
        if ($lastActivity !== '' && $lastActivity !== '0001-01-01T00:00:00Z') {
            $ts = strtotime($lastActivity);
            if (is_int($ts)) {
                return $ts;
            }
        }

        $createdOn = trim((string) ($ticket['Created_On'] ?? ''));
        if ($createdOn !== '' && $createdOn !== '0001-01-01T00:00:00Z') {
            $ts = strtotime($createdOn);
            if (is_int($ts)) {
                return $ts;
            }
        }

        return null;
    }

    /**
     * @param array<string, string|int|float|bool> $messageParams
     * @return array{
     *   recommendation_id: string,
     *   rule_id: string,
     *   ticket_no: string,
     *   message_key: string,
     *   message_params: array<string, string|int|float|bool>,
     *   priority: int,
     *   urgency_score: int,
     *   age_hours: int|null,
     *   source: string
     * }
     */
    private function createCandidate(
        string $ruleId,
        string $ticketNo,
        string $messageKey,
        array $messageParams,
        int $priority,
        int $urgencyScore,
        ?int $ageHours,
        string $source
    ): array {
        return [
            'recommendation_id' => $ruleId . '-' . $ticketNo,
            'rule_id' => $ruleId,
            'ticket_no' => $ticketNo,
            'message_key' => $messageKey,
            'message_params' => $messageParams,
            'priority' => $priority,
            'urgency_score' => max(0, $urgencyScore),
            'age_hours' => $ageHours,
            'source' => $source,
        ];
    }
}
