<?php
declare(strict_types=1);

final class TicketRepository {
    private const LIST_SORT = [
        'opened_at_ms',
        'updated_at_ms',
        'title',
        'rating',
        'app_version',
        'device_model',
        'owner_username',
    ];
    private static ?array $ticketColumns = null;

    public static function insert(array $payload): int {
        $err = TicketTagCatalog::validate(is_array($payload['tags'] ?? null) ? $payload['tags'] : []);
        if ($err !== null) {
            throw new InvalidArgumentException($err);
        }
        $pdo = Database::pdo();
        $device = is_array($payload['device'] ?? null) ? $payload['device'] : [];
        $app = is_array($payload['app'] ?? null) ? $payload['app'] : [];
        $tags = TicketTagCatalog::normalize($payload['tags'] ?? []);
        $tagsJson = json_encode($tags, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
        $json = json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
        if ($json === false || $tagsJson === false) {
            throw new RuntimeException('json_encode failed');
        }
        $now = (int) round(microtime(true) * 1000);
        $opened = (int) ($payload['opened_at_epoch_ms'] ?? $now);
        $companyId = Rbac::defaultCompanyId();
        $deviceId = DeviceRepository::upsertFromPayload($companyId, $device, 'auto');
        $title = trim((string) ($payload['title'] ?? ''));
        if ($title === '') {
            throw new InvalidArgumentException('title required');
        }
        $brief = trim((string) ($payload['brief'] ?? ''));
        $body = (string) ($payload['body'] ?? $brief);
        $rating = max(0, min(5, (int) ($payload['rating'] ?? 0)));
        $owner = trim((string) ($payload['owner'] ?? $payload['owner_username'] ?? ''));
        $verified = trim((string) ($payload['verified_by'] ?? $payload['verified_by_username'] ?? ''));
        $osName = device_os_name($device);
        $osVer = ticket_format_os_version($device);
        $stmt = $pdo->prepare(
            'INSERT INTO tickets (company_id, device_id, ticket_id, title, brief, body,
                opened_at_ms, created_at_ms, updated_at_ms, owner_username, verified_by_username,
                device_model, app_package, app_version, os_name, os_version, rating, tags_json, payload_json)
             VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
        );
        $stmt->execute([
            $companyId,
            $deviceId > 0 ? $deviceId : null,
            $payload['ticket_id'] ?? uniqid('tkt_', true),
            mb_substr($title, 0, 256),
            $brief !== '' ? $brief : null,
            $body,
            $opened,
            $now,
            $now,
            mb_substr($owner, 0, 64),
            $verified !== '' ? mb_substr($verified, 0, 64) : null,
            $device['model'] ?? null,
            $app['package'] ?? 'com.foxx.androidcast',
            $app['version_name'] ?? null,
            $osName !== '' ? $osName : null,
            $osVer !== '' ? $osVer : null,
            $rating,
            $tagsJson,
            $json,
        ]);
        return (int) $pdo->lastInsertId();
    }

    public static function listPage(int $page, int $perPage, string $sort, string $dir, string $tagFilter = ''): array {
        $pdo = Database::pdo();
        $page = max(1, $page);
        $perPage = max(1, min(200, $perPage));
        $dir = strtolower($dir) === 'asc' ? 'ASC' : 'DESC';
        $sortCol = in_array($sort, self::LIST_SORT, true) ? $sort : 'opened_at_ms';
        $offset = ($page - 1) * $perPage;
        $where = ['1=1'];
        $params = [];
        self::appendCompanyScope('company_id', $where, $params);
        if ($tagFilter !== '') {
            $where[] = 'tags_json LIKE ?';
            $params[] = '%"id":"' . str_replace(['%', '_'], ['\\%', '\\_'], $tagFilter) . '"%';
        }
        $whereSql = implode(' AND ', $where);
        $countStmt = $pdo->prepare("SELECT COUNT(*) FROM tickets WHERE $whereSql");
        $countStmt->execute($params);
        $total = (int) $countStmt->fetchColumn();
        $select = 'id, ticket_id, title, brief, opened_at_ms, updated_at_ms, created_at_ms,
                   owner_username, verified_by_username, device_model, app_package, app_version,
                   os_name, os_version, rating, tags_json';
        if (self::hasTicketColumn('workflow_state')) {
            $select .= ', workflow_state';
        }
        if (self::hasTicketColumn('assignees_json')) {
            $select .= ', assignees_json';
        }
        $sql = "SELECT $select
                FROM tickets WHERE $whereSql ORDER BY $sortCol $dir LIMIT $perPage OFFSET $offset";
        $stmt = $pdo->prepare($sql);
        $stmt->execute($params);
        $items = [];
        foreach ($stmt->fetchAll() as $row) {
            $items[] = self::mapListRow($row);
        }
        return [
            'items' => $items,
            'total' => $total,
            'page' => $page,
            'per_page' => $perPage,
            'sort' => $sortCol,
            'dir' => strtolower($dir),
        ];
    }

    public static function getById(int $id): ?array {
        $pdo = Database::pdo();
        $where = ['id = ?'];
        $params = [$id];
        self::appendCompanyScope('company_id', $where, $params);
        $stmt = $pdo->prepare(
            'SELECT * FROM tickets WHERE ' . implode(' AND ', $where) . ' LIMIT 1'
        );
        $stmt->execute($params);
        $row = $stmt->fetch();
        if (!$row) {
            return null;
        }
        return self::mapDetailRow($row);
    }

    public static function setCustomTags(int $id, array $tagsIn): void {
        $ticket = self::getById($id);
        if (!$ticket) {
            throw new RuntimeException('not found');
        }
        $err = TicketTagCatalog::validate($tagsIn);
        if ($err !== null) {
            throw new InvalidArgumentException($err);
        }
        $tags = TicketTagCatalog::normalize($tagsIn);
        $now = (int) round(microtime(true) * 1000);
        $pdo = Database::pdo();
        $stmt = $pdo->prepare('UPDATE tickets SET tags_json = ?, updated_at_ms = ? WHERE id = ?');
        $stmt->execute([
            json_encode($tags, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
            $now,
            $id,
        ]);
    }

    /** @param array<string, mixed> $fields */
    public static function update(int $id, array $fields): void {
        $ticket = self::getById($id);
        if (!$ticket) {
            throw new RuntimeException('not found');
        }
        $sets = [];
        $params = [];
        if (isset($fields['title'])) {
            $title = trim((string) $fields['title']);
            if ($title === '') {
                throw new InvalidArgumentException('title required');
            }
            $sets[] = 'title = ?';
            $params[] = mb_substr($title, 0, 256);
        }
        if (array_key_exists('brief', $fields)) {
            $sets[] = 'brief = ?';
            $params[] = trim((string) $fields['brief']) ?: null;
        }
        if (isset($fields['body'])) {
            $sets[] = 'body = ?';
            $params[] = (string) $fields['body'];
        }
        if (isset($fields['rating'])) {
            $sets[] = 'rating = ?';
            $params[] = max(0, min(5, (int) $fields['rating']));
        }
        if (isset($fields['owner_username'])) {
            $sets[] = 'owner_username = ?';
            $params[] = mb_substr(trim((string) $fields['owner_username']), 0, 64);
        }
        if (array_key_exists('verified_by_username', $fields)) {
            $v = trim((string) $fields['verified_by_username']);
            $sets[] = 'verified_by_username = ?';
            $params[] = $v !== '' ? mb_substr($v, 0, 64) : null;
        }
        if (isset($fields['workflow_state']) && self::hasTicketColumn('workflow_state')) {
            $state = TicketWorkflow::normalize((string) $fields['workflow_state']);
            $sets[] = 'workflow_state = ?';
            $params[] = $state;
            $tags = TicketWorkflow::mergeTagsForState(
                is_array($ticket['tags'] ?? null) ? $ticket['tags'] : [],
                $state
            );
            $sets[] = 'tags_json = ?';
            $params[] = json_encode($tags, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
        }
        if (array_key_exists('assignees', $fields) && self::hasTicketColumn('assignees_json')) {
            $assignees = self::normalizeAssignees($fields['assignees']);
            $err = UserRepository::validateUsernames(
                array_map(static fn (array $a): string => $a['username'], $assignees)
            );
            if ($err !== null) {
                throw new InvalidArgumentException($err);
            }
            $json = json_encode($assignees, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
            if ($json === false) {
                throw new RuntimeException('json_encode failed');
            }
            $sets[] = 'assignees_json = ?';
            $params[] = $json;
        }
        if ($sets === []) {
            return;
        }
        $now = (int) round(microtime(true) * 1000);
        $sets[] = 'updated_at_ms = ?';
        $params[] = $now;
        $params[] = $id;
        $pdo = Database::pdo();
        $stmt = $pdo->prepare('UPDATE tickets SET ' . implode(', ', $sets) . ' WHERE id = ?');
        $stmt->execute($params);
    }

    public static function listTagPresets(): array {
        return TicketTagCatalog::presets();
    }

    private static function appendCompanyScope(string $column, array &$where, array &$params): void {
        $allowed = Rbac::allowedCompanyIds();
        if ($allowed === null) {
            return;
        }
        if ($allowed === []) {
            $where[] = '0=1';
            return;
        }
        $placeholders = implode(',', array_fill(0, count($allowed), '?'));
        $where[] = "$column IN ($placeholders)";
        foreach ($allowed as $id) {
            $params[] = $id;
        }
    }

    private static function hasTicketColumn(string $column): bool {
        if (self::$ticketColumns === null) {
            self::$ticketColumns = Database::columnNames(Database::pdo(), 'tickets');
        }
        return in_array($column, self::$ticketColumns, true);
    }

    /** @param array<string, mixed> $row */
    private static function mapListRow(array $row): array {
        $tags = parse_report_tags($row['tags_json'] ?? '[]');
        $pkg = (string) ($row['app_package'] ?? 'com.foxx.androidcast');
        $ver = (string) ($row['app_version'] ?? '');
        $os = trim(((string) ($row['os_name'] ?? '')) . ' ' . ((string) ($row['os_version'] ?? '')));
        return [
            'id' => (int) $row['id'],
            'ticket_id' => (string) $row['ticket_id'],
            'title' => (string) $row['title'],
            'brief' => (string) ($row['brief'] ?? ''),
            'opened_at_ms' => (int) $row['opened_at_ms'],
            'updated_at_ms' => (int) $row['updated_at_ms'],
            'owner_username' => (string) ($row['owner_username'] ?? ''),
            'verified_by_username' => (string) ($row['verified_by_username'] ?? ''),
            'device_model' => (string) ($row['device_model'] ?? ''),
            'app_package' => $pkg,
            'app_version' => $ver,
            'os_name' => (string) ($row['os_name'] ?? ''),
            'os_version' => (string) ($row['os_version'] ?? ''),
            'env_brief' => ticket_env_brief($pkg, $ver, (string) ($row['os_name'] ?? ''), (string) ($row['os_version'] ?? '')),
            'rating' => (int) ($row['rating'] ?? 0),
            'tags' => $tags,
            'workflow_state' => TicketWorkflow::normalize((string) ($row['workflow_state'] ?? 'triage')),
            'assignees' => self::parseAssigneesJson($row['assignees_json'] ?? '[]'),
        ];
    }

    /** @param array<string, mixed> $row */
    private static function mapDetailRow(array $row): array {
        $list = self::mapListRow($row);
        $payload = json_decode($row['payload_json'] ?? '', true);
        $list['body'] = (string) ($row['body'] ?? '');
        $list['created_at_ms'] = (int) ($row['created_at_ms'] ?? 0);
        $list['payload'] = is_array($payload) ? $payload : [];
        $list['can_edit'] = Auth::canEditTicket($list);
        try {
            $list['comments'] = TicketCommentRepository::listForTicket((int) $row['id']);
        } catch (Throwable $e) {
            $list['comments'] = [];
        }
        try {
            $list['attachments'] = TicketAttachmentRepository::listForTicket((int) $row['id']);
        } catch (Throwable $e) {
            $list['attachments'] = [];
        }
        return $list;
    }

    /** @return list<array{username:string,role:string}> */
    public static function parseAssigneesJson(string $json): array {
        $data = json_decode($json, true);
        if (!is_array($data)) {
            return [];
        }
        return self::normalizeAssignees($data);
    }

    /**
     * @param mixed $raw
     * @return list<array{username:string,role:string}>
     */
    public static function normalizeAssignees($raw): array {
        if (!is_array($raw)) {
            return [];
        }
        $out = [];
        $seen = [];
        foreach ($raw as $item) {
            if (is_string($item)) {
                $item = ['username' => $item, 'role' => ''];
            }
            if (!is_array($item)) {
                continue;
            }
            $user = trim((string) ($item['username'] ?? ''));
            if ($user === '') {
                continue;
            }
            $key = strtolower($user);
            if (isset($seen[$key])) {
                continue;
            }
            $seen[$key] = true;
            $out[] = [
                'username' => mb_substr($user, 0, 64),
                'role' => mb_substr(trim((string) ($item['role'] ?? '')), 0, 32),
            ];
        }
        return $out;
    }
}

function ticket_format_os_version(array $device): string {
    $rel = (string) ($device['release'] ?? '');
    $sdk = (int) ($device['sdk_int'] ?? 0);
    if ($rel !== '' && $sdk > 0) {
        return $rel . ' (API ' . $sdk . ')';
    }
    if ($rel !== '') {
        return $rel;
    }
    if ($sdk > 0) {
        return 'API ' . $sdk;
    }
    return '';
}

function ticket_env_brief(string $pkg, string $version, string $osName, string $osVersion): string {
    $app = str_contains($pkg, 'androidcast') ? 'Android Cast' : ($pkg !== '' ? $pkg : 'app');
    $line = trim($app . ($version !== '' ? ' ' . $version : ''));
    $os = trim($osName . ($osVersion !== '' ? ' · ' . $osVersion : ''));
    return trim($line . ($os !== '' ? ' · ' . $os : ''));
}
