<?php

declare(strict_types=1);

require_once __DIR__ . '/storage.php';
require_once __DIR__ . '/chat_store.php';
require_once __DIR__ . '/bitrix.php';

function analyticsInputs(array $input): array
{
    foreach (['benchmark' => [0, 100], 'expense' => [0, 1000000000]] as $key => [$min, $max]) {
        if (!isset($input[$key]) || !is_numeric($input[$key]) || !is_finite((float) $input[$key])
            || (float) $input[$key] < $min || (float) $input[$key] > $max) {
            throw new InvalidArgumentException('Проверьте эталонный показатель и расход. Если открыта старая форма, обновите страницу приложения.');
        }
    }
    $timezone = new DateTimeZone((string) config('ANALYTICS_TIMEZONE', 'Europe/Moscow'));
    foreach (['date_from', 'date_to'] as $key) {
        $value = $input[$key] ?? null;
        $date = is_string($value) ? DateTimeImmutable::createFromFormat('!Y-m-d', $value, $timezone) : false;
        if (!$date || $date->format('Y-m-d') !== $value) throw new InvalidArgumentException('Укажите корректные даты начала и окончания периода.');
    }
    if ($input['date_from'] > $input['date_to']) throw new InvalidArgumentException('Дата начала периода не может быть позже даты окончания.');
    return ['benchmark' => (float) $input['benchmark'] / 100, 'expense' => round((float) $input['expense'], 2),
        'date_from' => $input['date_from'], 'date_to' => $input['date_to'], 'timezone' => $timezone->getName()];
}

function analyticsSamePeriod(array $left, array $right): bool
{
    foreach (['date_from', 'date_to', 'timezone'] as $key) if (($left[$key] ?? null) !== ($right[$key] ?? null)) return false;
    return true;
}

function analyticsDealInPeriod(array $deal, array $inputs): ?bool
{
    if (!isset($inputs['date_from'], $inputs['date_to'])) return true;
    $created = $deal['created_at'] ?? '';
    if (!is_string($created) || $created === '') return null;
    try {
        $zone = new DateTimeZone($inputs['timezone'] ?? 'Europe/Moscow');
        $time = new DateTimeImmutable($created, $zone);
        $start = new DateTimeImmutable($inputs['date_from'] . ' 00:00:00', $zone);
        $end = (new DateTimeImmutable($inputs['date_to'] . ' 00:00:00', $zone))->modify('+1 day');
        return $time >= $start && $time < $end;
    } catch (Throwable $error) { return null; }
}

function analyticsBotIds(): array
{
    return array_values(array_unique(array_filter(array_map('strval', array_merge(
        [(int) config('BITRIX_AI_BOT_ID', 0), (int) config('BITRIX_OPENLINE_BOT_ID', 0)],
        (array) config('MANAGED_OPEN_LINE_BOT_IDS', [])
    )), static fn (string $id): bool => ctype_digit($id) && (int) $id > 0)));
}

// Keep only metrics and IDs: the analytics job never stores message text or credentials.
function analyticsCompactChat(array $chat, string $key, array $bots): ?array
{
    $context = (array) ($chat['context'] ?? []);
    $line = (string) ($context['open_line_id'] ?? '');
    $bot = (string) ($context['bot_id'] ?? '');
    if (!ctype_digit($line) || (int) $line <= 0 || !in_array($bot, $bots, true)) return null;
    $messages = array_values(array_filter((array) ($chat['messages'] ?? []), static fn ($m): bool => is_array($m) && in_array($m['role'] ?? '', ['user', 'assistant'], true)));
    if ($messages === [] && empty($chat['assistant_has_replied'])) return null;
    $chatId = (string) ($context['chat_id'] ?? '');
    $dialog = (string) ($context['dialog_id'] ?? '');
    if ($chatId === '' && preg_match('/^chat(\d+)$/', $dialog, $match)) $chatId = $match[1];
    $times = array_values(array_filter(array_map(static fn (array $m): int => (int) strtotime((string) ($m['created_at'] ?? '')), $messages)));
    $deal = (string) ($context['deal_id'] ?? '');
    return [
        'key' => $chatId !== '' ? 'chat:' . $chatId : $key, 'chat_id' => $chatId, 'line' => $line,
        'deal' => ctype_digit($deal) && (int) $deal > 0 ? $deal : '',
        'updated' => (string) ($chat['updated_at'] ?? ''),
        'first' => $times ? min($times) : 0,
    ];
}

function analyticsCollectChats(): array
{
    $file = (string) config('CHAT_HISTORY_FILE');
    if (!chatStoreIsSplit() && is_file($file) && filesize($file) > 40 * 1024 * 1024) {
        throw new RuntimeException('История превышает безопасный размер для онлайн-отчёта. Требуется фоновый сбор аналитики.');
    }
    $bots = analyticsBotIds();
    $chats = [];
    $accept = static function (array $chat, string $key) use (&$chats, $bots): void {
        $compact = analyticsCompactChat($chat, $key, $bots);
        if ($compact === null) return;
        $old = $chats[$compact['key']] ?? null;
        if ($old === null || (strtotime($compact['updated']) ?: 0) > (strtotime($old['updated']) ?: 0)) $chats[$compact['key']] = $compact;
    };
    foreach (chatStoreIterate(true) as $key => $chat) {
        $accept($chat, (string) $key);
        if (count($chats) > 20000) throw new RuntimeException('Для более 20 000 диалогов требуется фоновый отчёт.');
    }
    return array_values($chats);
}

function analyticsNewJob(array $inputs, string $userId): array
{
    $chats = analyticsCollectChats();
    return [
        'version' => 5, 'id' => bin2hex(random_bytes(16)), 'user_id' => $userId, 'created_at' => date('c'),
        'inputs' => $inputs, 'chats' => [], 'deals' => [], 'names' => [],
        'candidate_chats' => $chats, 'selected_ids' => [], 'phase' => 'selecting',
        'cursor' => 0, 'results' => [], 'communication' => [], 'bots' => analyticsBotIds(),
        'tasks' => [['type' => 'select', 'start' => 0]],
        'qualification_stage' => (string) config('TECHNIQUE_QUALIFIED_STAGE_ID', '37'),
        'qualification_category' => (string) config('RETAIL_DEAL_CATEGORY_ID', 0),
    ];
}

function analyticsSelectPeriodPage(array $job): array
{
    $task = $job['tasks'][$job['cursor']];
    $zone = new DateTimeZone($job['inputs']['timezone']);
    $start = new DateTimeImmutable($job['inputs']['date_from'] . ' 00:00:00', $zone);
    $end = (new DateTimeImmutable($job['inputs']['date_to'] . ' 00:00:00', $zone))->modify('+1 day');
    // Filter in Bitrix before requesting any individual deals, stage history or chat history.
    $response = bitrixCall('crm.deal.list', ['order' => ['ID' => 'ASC'],
        'filter' => ['>=DATE_CREATE' => $start->format('c'), '<DATE_CREATE' => $end->format('c')],
        'select' => ['ID', 'DATE_CREATE'], 'start' => $task['start']]);
    if (!is_array($response['result'] ?? null)) throw new RuntimeException('Cannot select deals for the requested period.');
    $candidates = array_fill_keys(array_filter(array_column($job['candidate_chats'], 'deal')), true);
    foreach ($response['result'] as $deal) {
        if (!is_array($deal) || !ctype_digit((string) ($deal['ID'] ?? ''))
            || analyticsDealInPeriod(['created_at' => $deal['DATE_CREATE'] ?? ''], $job['inputs']) !== true) {
            throw new RuntimeException('CRM returned a deal outside the requested creation period.');
        }
        $id = (string) $deal['ID'];
        if (isset($candidates[$id])) $job['selected_ids'][$id] = true;
    }
    $job['cursor']++;
    if (isset($response['next'])) {
        $next = filter_var($response['next'], FILTER_VALIDATE_INT);
        if ($next === false || $next <= $task['start']) throw new RuntimeException('Invalid CRM selection pagination.');
        $job['tasks'][] = ['type' => 'select', 'start' => $next];
        return $job;
    }
    $job['chats'] = array_values(array_filter($job['candidate_chats'], static fn (array $chat): bool => isset($job['selected_ids'][$chat['deal']])));
    $job['deals'] = array_map('strval', array_keys($job['selected_ids']));
    $lines = array_values(array_unique(array_column($job['chats'], 'line')));
    foreach (array_chunk($lines, 50) as $chunk) {
        $response = bitrixCallRaw('imopenlines.config.list.get', ['PARAMS' => [
            'select' => ['ID', 'LINE_NAME'], 'filter' => ['ID' => $chunk], 'limit' => 50,
        ]]);
        foreach ((array) ($response['result'] ?? []) as $line) {
            if (is_array($line) && isset($line['ID'], $line['LINE_NAME'])) $job['names'][(string) $line['ID']] = (string) $line['LINE_NAME'];
        }
    }
    $job['selection_steps'] = $job['cursor'];
    $job['tasks'] = array_merge($job['tasks'],
        array_map(static fn (string $id): array => ['type' => 'deal', 'id' => $id], $job['deals']),
        array_map(static fn (array $chat): array => ['type' => 'chat', 'id' => $chat['chat_id'], 'key' => $chat['key'], 'deal' => $chat['deal']], $job['chats']));
    $job['phase'] = 'calculating';
    unset($job['candidate_chats'], $job['selected_ids']);
    if ($job['cursor'] >= count($job['tasks'])) $job['completed_at'] = date('c');
    return $job;
}

function analyticsAdvanceJob(array $job): array
{
    $next = $job['tasks'][$job['cursor']] ?? null;
    if (($next['type'] ?? '') === 'select') return analyticsSelectPeriodPage($job);
    // Session history responses can be large; never fetch twenty of them at once.
    $limit = ($next['type'] ?? '') === 'chat' ? 4 : 20;
    $tasks = [];
    foreach (array_slice($job['tasks'], (int) $job['cursor'], $limit) as $task) {
        if ($task['type'] !== $next['type']) break;
        $tasks[] = $task;
    }
    if ($tasks === []) return $job;
    $commands = [];
    foreach ($tasks as $task) {
        $id = $task['id'];
        if ($task['type'] === 'chat') {
            if (analyticsDealInPeriod($job['results'][$task['deal']] ?? [], $job['inputs']) !== true) continue;
            if (ctype_digit($id) && (int) $id > 0) $commands['chat_' . $id] = 'imopenlines.session.history.get?' . http_build_query(['CHAT_ID' => $id]);
            continue;
        }
        $commands['deal_' . $id] = 'crm.deal.get?' . http_build_query(['id' => $id]);
        $commands['qualification_' . $id] = 'crm.stagehistory.list?' . http_build_query([
            'entityTypeId' => 2, 'order' => ['ID' => 'ASC'],
            'filter' => ['OWNER_ID' => $id, 'STAGE_ID' => $job['qualification_stage'], 'CATEGORY_ID' => $job['qualification_category']],
            'select' => ['ID', 'OWNER_ID', 'CREATED_TIME', 'STAGE_ID', 'CATEGORY_ID'],
        ]);
    }
    $response = $commands ? bitrixCall('batch', ['halt' => 0, 'cmd' => $commands]) : [];
    $batch = (array) ($response['result'] ?? []);
    $results = (array) ($batch['result'] ?? []);
    foreach ($tasks as $task) {
        $id = $task['id'];
        if ($task['type'] === 'chat') {
            $dealId = (string) ($task['deal'] ?? '');
            $job['communication'][$task['key']] = analyticsCommunication(
                $results['chat_' . $id] ?? [], $job['bots'], $job['results'][$dealId]['qualified_at'] ?? null
            );
            continue;
        }
        $deal = $results['deal_' . $id] ?? null;
        $stageResult = $results['qualification_' . $id] ?? null;
        if (!is_array($deal) || !isset($deal['ID'])) {
            $job['results'][$id] = ['error' => true];
            continue;
        }
        $items = is_array($stageResult) ? (array) ($stageResult['items'] ?? $stageResult) : [];
        $qualifiedAt = null;
        foreach ($items as $item) {
            if (!is_array($item)) continue;
            $time = (string) ($item['CREATED_TIME'] ?? $item['createdTime'] ?? '');
            if ($time !== '' && ($qualifiedAt === null || strtotime($time) < strtotime($qualifiedAt))) $qualifiedAt = $time;
        }
        $currentlyQualified = (string) ($deal['STAGE_ID'] ?? '') === $job['qualification_stage']
            && (string) ($deal['CATEGORY_ID'] ?? '') === $job['qualification_category'];
        $job['results'][$id] = [
            'created_at' => (string) ($deal['DATE_CREATE'] ?? ''),
            'qualified' => $qualifiedAt !== null || $currentlyQualified ? true : (is_array($stageResult) ? false : null),
            'qualified_at' => $qualifiedAt, 'semantic' => (string) ($deal['STAGE_SEMANTIC_ID'] ?? ''),
            'assigned' => (string) ($deal['ASSIGNED_BY_ID'] ?? ''),
            'amount' => isset($deal['OPPORTUNITY']) && is_numeric($deal['OPPORTUNITY']) ? (float) $deal['OPPORTUNITY'] : null,
            'currency' => (string) ($deal['CURRENCY_ID'] ?? ''),
        ];
    }
    $job['cursor'] += count($tasks);
    if ($job['cursor'] >= count($job['tasks'])) $job['completed_at'] = date('c');
    return $job;
}

function analyticsCommunication(array $session, array $bots, ?string $qualifiedAt = null): ?array
{
    if (!isset($session['message'], $session['users'], $session['sessionId'])) return null;
    $client = 0; $manager = false; $botSeen = false; $first = 0; $before = 0; $totalMessages = 0;
    $qualificationTime = $qualifiedAt !== null ? strtotime($qualifiedAt) : false;
    foreach ((array) $session['message'] as $message) {
        $sender = (string) ($message['senderid'] ?? '0');
        $text = trim(strip_tags((string) ($message['text'] ?? '')));
        $params = (array) ($message['params'] ?? []);
        if ($sender === '0' || preg_match('/^(?:\[Системное сообщение\]|Входящий звонок$|Исходящий звонок$|Пропущенный звонок$)/iu', $text)
            || in_array($params['SYSTEM'] ?? false, [true, 'Y', 1, '1'], true)) continue;
        $user = $session['users'][$sender] ?? null;
        $time = strtotime((string) ($message['date'] ?? ''));
        if ($time === false || !is_array($user)) return null;
        if (in_array($sender, $bots, true)) $botSeen = true;
        elseif (($user['connector'] ?? false) === true) $client++;
        elseif (($user['bot'] ?? false) === true || ($user['type'] ?? '') === 'bot') continue;
        elseif (($user['connector'] ?? null) === false && ($user['extranet'] ?? null) === false) $manager = true;
        else return null;
        $totalMessages++;
        if (!$first || $time < $first) $first = $time;
        if ($qualificationTime !== false && $time <= $qualificationTime) $before++;
    }
    // A later human-only session must not be attributed to a previous AI session.
    if (!$botSeen) return null;
    return ['session_id' => (string) $session['sessionId'], 'client' => $client, 'manager' => $manager, 'total_messages' => $totalMessages,
        'before_qualification' => $qualificationTime !== false && $first && $qualificationTime >= $first ? $before : null];
}

function analyticsReport(array $job): array
{
    $groups = []; $attribution = []; $first = 0; $unknownDeals = 0;
    $chats = $job['chats'];
    $unknownPeriod = 0;
    if (isset($job['inputs']['date_from'], $job['inputs']['date_to'])) {
        $chats = array_values(array_filter($chats, static function (array $chat) use ($job, &$unknownPeriod): bool {
            $included = analyticsDealInPeriod($job['results'][$chat['deal']] ?? [], $job['inputs']);
            if ($included === null) $unknownPeriod++;
            return $included === true;
        }));
    }
    usort($chats, static fn (array $a, array $b): int => $a['first'] <=> $b['first']);
    foreach ($chats as $chat) {
        $line = $chat['line'];
        $groups[$line] ??= ['line' => $line, 'accepted' => 0, 'deal_ids' => [], 'chats' => []];
        $groups[$line]['accepted']++;
        $groups[$line]['chats'][] = $chat;
        if ($chat['first'] && (!$first || $chat['first'] < $first)) $first = $chat['first'];
        if ($chat['deal'] === '') { $unknownDeals++; continue; }
        // Revenue and deal outcomes are attributed once, even across several chats/lines.
        if (!isset($attribution[$chat['deal']])) {
            $attribution[$chat['deal']] = $line;
            $groups[$line]['deal_ids'][] = $chat['deal'];
        }
    }
    $build = static function (array $group) use ($job): array {
        $row = array_merge($job['inputs'], [
            'line_id' => (string) $group['line'], 'label' => $group['line'] === 'total' ? 'Итого' : $group['line'] . ' / ' . ($job['names'][$group['line']] ?? 'Открытая линия'),
            'accepted' => $group['accepted'], 'qualified' => 0, 'working' => 0, 'won' => 0,
            'revenue' => 0.0, 'expense' => null, 'chat_cost' => null, 'qualification_cost' => null, 'drr' => null,
            'average_length' => null, 'average_client' => null, 'average_before_qualification' => null, 'manager_chats' => null, 'manager_rate' => null,
        ]);
        $currencies = []; $incomplete = false;
        foreach ($group['chats'] as $chat) if ($chat['deal'] === '') $incomplete = true;
        foreach ($group['deal_ids'] as $id) {
            $deal = $job['results'][$id] ?? ['error' => true];
            if (!empty($deal['error'])) { $incomplete = true; continue; }
            if ($deal['qualified'] === null) $row['qualified'] = null;
            elseif ($deal['qualified'] && $row['qualified'] !== null) $row['qualified']++;
            if ($deal['semantic'] === 'P' && in_array($deal['assigned'], $job['bots'], true)) $row['working']++;
            if (!in_array($deal['semantic'], ['P', 'S', 'F'], true)) $incomplete = true;
            if ($deal['semantic'] === 'S') {
                $row['won']++;
                if ($deal['amount'] === null || $deal['currency'] === '') $row['revenue'] = null;
                elseif ($row['revenue'] !== null) $row['revenue'] += $deal['amount'];
                $currencies[$deal['currency']] = true;
            }
        }
        if ($incomplete) foreach (['qualified', 'working', 'won', 'revenue'] as $key) $row[$key] = null;
        if (count($currencies) > 1) $row['revenue'] = null;
        $row['currency'] = count($currencies) === 1 ? (string) array_key_first($currencies) : null;
        $clientMessages = 0; $managerChats = 0; $communicationComplete = true; $beforeTotal = 0; $beforeCount = 0; $beforeComplete = true;
        $totalMessages = 0; $lengthComplete = true;
        foreach ($group['chats'] as $chat) {
            $communication = $job['communication'][$chat['key']] ?? null;
            if ($communication === null) { $communicationComplete = false; $beforeComplete = false; continue; }
            if (!isset($communication['total_messages'])) $lengthComplete = false;
            else $totalMessages += $communication['total_messages'];
            $clientMessages += $communication['client'];
            if ($communication['manager']) $managerChats++;
            $deal = $job['results'][$chat['deal']] ?? [];
            if (!empty($deal['qualified'])) {
                if (!isset($communication['before_qualification'])) { $beforeComplete = false; continue; }
                $beforeTotal += $communication['before_qualification'];
                $beforeCount++;
            }
        }
        if ($communicationComplete && $row['accepted']) {
            if ($lengthComplete) $row['average_length'] = $totalMessages / $row['accepted'];
            $row['average_client'] = $clientMessages / $row['accepted'];
            $row['manager_chats'] = $managerChats;
            $row['manager_rate'] = $managerChats / $row['accepted'];
        }
        if ($beforeComplete && $beforeCount && !$incomplete && $row['qualified'] !== null) $row['average_before_qualification'] = $beforeTotal / $beforeCount;
        foreach (['conversion' => 'qualified', 'working_rate' => 'working', 'won_rate' => 'won'] as $rate => $key) {
            $row[$rate] = $row[$key] !== null && $row['accepted'] > 0 ? $row[$key] / $row['accepted'] : null;
        }
        $row['deviation'] = $row['conversion'] !== null ? $row['conversion'] - $row['benchmark'] : null;
        // Old cached jobs use incompatible inputs; never reinterpret price/length as total expense.
        unset($row['average_chat_price']);
        $expense = $group['expense'] ?? null;
        if ($expense !== null) {
            $row['expense'] = $expense;
            $row['chat_cost'] = $row['accepted'] > 0 ? $expense / $row['accepted'] : null;
            $row['qualification_cost'] = $row['qualified'] !== null && $row['qualified'] > 0
                ? round($row['expense'] / $row['qualified'], 2) : null;
            $row['drr'] = $row['currency'] === 'RUB' && $row['revenue'] !== null && $row['revenue'] > 0
                ? $row['expense'] / $row['revenue'] : null;
        }
        return $row;
    };
    ksort($groups, SORT_NATURAL);
    $expense = $job['inputs']['expense'] ?? null;
    if ($expense !== null && count($chats) > 0) {
        // Round cumulative shares to cents so row expenses reconcile exactly to the entered total.
        $cents = (int) round($expense * 100);
        $cumulative = 0; $allocated = 0;
        foreach ($groups as &$group) {
            $cumulative += $group['accepted'];
            $target = (int) round($cents * ($cumulative / count($chats)));
            $group['expense'] = ($target - $allocated) / 100;
            $allocated = $target;
        }
        unset($group);
    }
    $rows = array_map($build, array_values($groups));
    $total = $build(['line' => 'total', 'accepted' => count($chats), 'deal_ids' => array_keys($attribution), 'chats' => $chats, 'expense' => $expense]);
    $errors = count(array_filter($job['results'], static fn (array $r): bool => !empty($r['error'])));
    $warnings = [
        'Расход введён в рублях на весь отчёт и распределён между открытыми линиями пропорционально обращениям. Средняя цена чата = расход / обращения; стоимость квалификации = расход / квалификации; ДРР = расход / доход. Сумма расходов по строкам равна итогу. При нулевом знаменателе или доходе не в RUB показатель не рассчитывается.',
        'Коммуникация рассчитана по последней доступной сессии открытой линии с сообщениями бота. Средняя длина чата = количество сообщений клиента, бота и менеджера / количество чатов. Служебные сообщения исключены. Вмешательство менеджера означает наличие его сообщения, а не просто смену ответственного. Если история недоступна хотя бы для одного чата строки, зависимые показатели отмечены «Нет данных».',
        'Закрытые сделки — успешно завершённые сделки с участием бота, даже если завершил их менеджер. Провальные и незавершённые сделки сюда не входят. Доля закрытых успешно = их количество / количество обращений. Эталон сравнивается с конверсией в квалификацию; отклонение показано в процентных пунктах.',
        'Квалификация учитывается по истории стадий CRM, независимо от того, кто переместил сделку. В работе на ИИ: открытая сделка с ответственным ботом. Доход: текущая сумма успешных сделок; разные валюты не суммируются.',
        'Одна сделка учитывается один раз, в первой сохранившейся открытой линии. Обращения считаются по уникальным чатам с подтверждённым участием бота; личные чаты не включены.',
    ];
    if ($expense === null) $warnings[] = 'Этот расчёт создан со старым параметром цены чата. Введите расход и запустите расчёт заново; финансовые показатели старого расчёта не пересчитываются автоматически.';
    if ($unknownPeriod) $warnings[] = "Не удалось подтвердить дату создания сделки для $unknownPeriod диалогов. Они исключены из выборки; отчёт может быть неполным.";
    if ($errors || $unknownDeals) $warnings[] = "Не удалось прочитать сделок: $errors; диалогов без связи со сделкой: $unknownDeals. Зависимые показатели не заменены нулями.";
    return [
        'rows' => $rows, 'total' => $total, 'warnings' => $warnings, 'generated_at' => $job['completed_at'] ?? date('c'),
        'scope' => (isset($job['inputs']['date_from'], $job['inputs']['date_to'])
            ? 'Дата создания сделки: ' . $job['inputs']['date_from'] . ' — ' . $job['inputs']['date_to'] . ' включительно (' . $job['inputs']['timezone'] . '). ' : '')
            . 'Срез по сохранившимся диалогам бота. Не полный архив за всё время. Состояния CRM и доход успешных сделок проверены при расчёте, а не на конец периода. Сообщения выбранных сделок не ограничены датами периода.',
    ];
}

function analyticsJobResponse(array $job): array
{
    $done = $job['cursor'] >= count($job['tasks']);
    return ['ok' => true, 'job' => $job['id'], 'processed' => $job['cursor'], 'total' => count($job['tasks']), 'done' => $done,
        'phase' => $job['phase'] ?? 'calculating', 'selection_steps' => $job['selection_steps'] ?? 0,
        'selected_deals' => count($job['selected_ids'] ?? $job['deals'] ?? [])]
        + ($done ? ['report' => analyticsReport($job)] : []);
}
