<?php
declare(strict_types=1);

final class TicketAttachmentRepository {
    private const MAX_BYTES = 16777216; // 16 MiB

    /** @return list<string> */
    private static function allowedExtensions(): array {
        return [
            'png', 'jpg', 'jpeg', 'gif', 'webp', 'svg',
            'pdf', 'txt', 'log', 'md', 'json', 'xml', 'zip',
            'mp4', 'webm', 'mp3', 'wav',
        ];
    }

    public static function storageRoot(): string {
        $dir = dirname(__DIR__) . '/storage/ticket_attachments';
        if (!is_dir($dir)) {
            @mkdir($dir, 0775, true);
        }
        return $dir;
    }

    public static function listForTicket(int $ticketId): array {
        // Do not call TicketRepository::getById here — avoids recursion from mapDetailRow.
        $pdo = Database::pdo();
        $stmt = $pdo->prepare(
            'SELECT * FROM ticket_attachments WHERE ticket_id = ? ORDER BY created_at_ms ASC, id ASC'
        );
        $stmt->execute([$ticketId]);
        $bp = Auth::basePath();
        $out = [];
        foreach ($stmt->fetchAll() as $row) {
            $out[] = self::mapRow($row, $bp);
        }
        return $out;
    }

    public static function getById(int $id): ?array {
        $pdo = Database::pdo();
        $stmt = $pdo->prepare('SELECT * FROM ticket_attachments WHERE id = ? LIMIT 1');
        $stmt->execute([$id]);
        $row = $stmt->fetch();
        if (!$row) {
            return null;
        }
        $ticket = TicketRepository::getById((int) $row['ticket_id']);
        if (!$ticket) {
            return null;
        }
        return self::mapRow($row, Auth::basePath());
    }

    public static function addLink(int $ticketId, string $author, string $url, string $title = ''): array {
        $ticket = TicketRepository::getById($ticketId);
        if (!$ticket) {
            throw new RuntimeException('not found');
        }
        $url = trim($url);
        if ($url === '' || !filter_var($url, FILTER_VALIDATE_URL)) {
            throw new InvalidArgumentException('valid url required');
        }
        $scheme = strtolower(parse_url($url, PHP_URL_SCHEME) ?? '');
        if (!in_array($scheme, ['http', 'https'], true)) {
            throw new InvalidArgumentException('http or https url required');
        }
        $provider = TicketAttachmentProvider::detectFromUrl($url);
        if ($title === '') {
            $title = TicketAttachmentProvider::labelFor($provider);
        }
        $now = (int) round(microtime(true) * 1000);
        $pdo = Database::pdo();
        $stmt = $pdo->prepare(
            'INSERT INTO ticket_attachments (ticket_id, kind, title, external_url, provider,
                storage_name, original_name, mime_type, size_bytes, created_by, created_at_ms)
             VALUES (?, \'link\', ?, ?, ?, NULL, NULL, NULL, NULL, ?, ?)'
        );
        $stmt->execute([
            $ticketId,
            mb_substr($title, 0, 256),
            $url,
            $provider,
            mb_substr(trim($author), 0, 64),
            $now,
        ]);
        self::touchTicket($ticketId, $now);
        return self::getById((int) $pdo->lastInsertId()) ?? [];
    }

    /** @param array{name:string,type:string,tmp_name:string,error:int,size:int} $file */
    public static function addFile(int $ticketId, string $author, array $file, string $title = ''): array {
        $ticket = TicketRepository::getById($ticketId);
        if (!$ticket) {
            throw new RuntimeException('not found');
        }
        if (($file['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {
            throw new InvalidArgumentException('upload failed');
        }
        $size = (int) ($file['size'] ?? 0);
        if ($size <= 0 || $size > self::MAX_BYTES) {
            throw new InvalidArgumentException('file too large (max 16 MiB)');
        }
        $original = basename((string) ($file['name'] ?? 'file'));
        $ext = strtolower(pathinfo($original, PATHINFO_EXTENSION));
        if ($ext === '' || !in_array($ext, self::allowedExtensions(), true)) {
            throw new InvalidArgumentException('file type not allowed');
        }
        $mime = (string) ($file['type'] ?? 'application/octet-stream');
        $storageName = bin2hex(random_bytes(16)) . '.' . $ext;
        $dir = self::storageRoot() . '/' . $ticketId;
        if (!is_dir($dir)) {
            @mkdir($dir, 0775, true);
        }
        $dest = $dir . '/' . $storageName;
        if (!move_uploaded_file((string) $file['tmp_name'], $dest)) {
            throw new RuntimeException('could not store file');
        }
        if ($title === '') {
            $title = $original;
        }
        $now = (int) round(microtime(true) * 1000);
        $pdo = Database::pdo();
        $stmt = $pdo->prepare(
            'INSERT INTO ticket_attachments (ticket_id, kind, title, external_url, provider,
                storage_name, original_name, mime_type, size_bytes, created_by, created_at_ms)
             VALUES (?, \'file\', ?, NULL, ?, ?, ?, ?, ?, ?, ?)'
        );
        $stmt->execute([
            $ticketId,
            mb_substr($title, 0, 256),
            TicketAttachmentProvider::GENERIC,
            $storageName,
            mb_substr($original, 0, 256),
            mb_substr($mime, 0, 128),
            $size,
            mb_substr(trim($author), 0, 64),
            $now,
        ]);
        self::touchTicket($ticketId, $now);
        return self::getById((int) $pdo->lastInsertId()) ?? [];
    }

    public static function delete(int $id, string $username): void {
        $att = self::getById($id);
        if (!$att) {
            throw new RuntimeException('not found');
        }
        $ticket = TicketRepository::getById((int) $att['ticket_id']);
        if (!$ticket) {
            throw new RuntimeException('not found');
        }
        $can = !empty($ticket['can_edit'])
            || strcasecmp((string) ($att['created_by'] ?? ''), $username) === 0
            || Auth::canEditTags();
        if (!$can) {
            throw new RuntimeException('forbidden');
        }
        if ($att['kind'] === 'file' && !empty($att['storage_name'])) {
            $path = self::storageRoot() . '/' . $att['ticket_id'] . '/' . $att['storage_name'];
            if (is_file($path)) {
                @unlink($path);
            }
        }
        $pdo = Database::pdo();
        $pdo->prepare('DELETE FROM ticket_attachments WHERE id = ?')->execute([$id]);
        self::touchTicket((int) $att['ticket_id'], (int) round(microtime(true) * 1000));
    }

    public static function filePath(array $attachment): ?string {
        if (($attachment['kind'] ?? '') !== 'file' || empty($attachment['storage_name'])) {
            return null;
        }
        $path = self::storageRoot() . '/' . (int) $attachment['ticket_id'] . '/' . $attachment['storage_name'];
        return is_file($path) ? $path : null;
    }

    private static function touchTicket(int $ticketId, int $now): void {
        Database::pdo()->prepare('UPDATE tickets SET updated_at_ms = ? WHERE id = ?')->execute([$now, $ticketId]);
    }

    /** @param array<string, mixed> $row */
    private static function mapRow(array $row, string $basePath): array {
        $id = (int) $row['id'];
        $kind = (string) $row['kind'];
        $provider = (string) ($row['provider'] ?? TicketAttachmentProvider::GENERIC);
        $item = [
            'id' => $id,
            'ticket_id' => (int) $row['ticket_id'],
            'kind' => $kind,
            'title' => (string) ($row['title'] ?? ''),
            'external_url' => (string) ($row['external_url'] ?? ''),
            'provider' => $provider,
            'provider_label' => TicketAttachmentProvider::labelFor($provider),
            'original_name' => (string) ($row['original_name'] ?? ''),
            'mime_type' => (string) ($row['mime_type'] ?? ''),
            'size_bytes' => (int) ($row['size_bytes'] ?? 0),
            'created_by' => (string) ($row['created_by'] ?? ''),
            'created_at_ms' => (int) ($row['created_at_ms'] ?? 0),
            'download_url' => $kind === 'file'
                ? $basePath . '/api/ticket_attachment.php?id=' . $id
                : null,
        ];
        return $item;
    }
}
