<?php

require_once __DIR__ . '/ThesisFileHandler.php';

/**
 * ThesisEditController
 *
 * Centralises all data-fetching and mutation logic for the admin thesis-edit
 * workflow (admin/edit.php + admin/actions/edit.php).
 *
 * Responsibilities:
 *  - Loading thesis data and lookup tables for the edit form view
 *  - Validating and persisting POST submissions (thesis metadata, authors,
 *    jury, languages, formats, tags, banner)
 *  - WCAG 3.3.1: mapping validation exceptions to autofocus field hints
 *
 * The class has NO output side-effects; all redirects, flash writes, and
 * template rendering stay in the thin dispatcher files so the view layer
 * remains easy to inspect and modify.
 */
class ThesisEditController
{
    use ThesisFileHandler;

    private Database $db;

    public function __construct(Database $db)
    {
        $this->db = $db;
    }

    // ── Factory ───────────────────────────────────────────────────────────────

    /**
     * Convenience factory — instantiates Database and returns a ready
     * controller.  Accepts an optional existing Database instance so callers
     * that already hold one (e.g. during testing) can avoid a second
     * connection.
     */
    public static function create(?Database $db = null): self
    {
        require_once APP_ROOT . '/src/Database.php';
        require_once APP_ROOT . '/src/ErrorHandler.php';

        return new self($db ?? Database::getInstance());
    }

    // ── Read / view data ─────────────────────────────────────────────────────

    /**
     * Load all data required to render the edit form.
     *
     * Returns a flat array of view variables:
     *   - 'thesis'              – thesis row (from getThesis)
     *   - 'currentLanguages'   – int[]
     *   - 'currentFormats'     – int[]
     *   - 'jury'               – jury rows
     *   - 'currentFiles'       – all thesis_files rows (cover + thesis files)
     *   - 'currentCover'       – single thesis_files row for cover, or null
     *   - 'orientations'       – lookup rows
     *   - 'apPrograms'         – lookup rows
     *   - 'finalityTypes'      – lookup rows
     *   - 'languages'          – lookup rows
     *   - 'formatTypes'        – lookup rows
     *   - 'licenseTypes'       – lookup rows
     *   - 'accessTypes'        – lookup rows
     *   - 'currentLicenseId'   – int|null
     *   - 'currentAccessTypeId'– int|null
     *   - 'currentContextNote' – string
     *   - 'pageTitle'          – string
     *
     * @throws Exception if the thesis is not found or a DB error occurs.
     */
    public function load(int $thesisId): array
    {
        if ($thesisId <= 0) {
            throw new InvalidArgumentException('ID invalide');
        }

        $thesis = $this->db->getThesis($thesisId);
        if (!$thesis) {
            throw new RuntimeException('TFE non trouvé');
        }

        $currentLanguages = $this->db->getThesisLanguageIds($thesisId);
        $currentFormats   = $this->db->getThesisFormatIds($thesisId);
        $jury             = $this->db->getThesisJury($thesisId);
        $currentFiles     = $this->db->getThesisFiles($thesisId);

        // Separate out the cover entry for convenience
        $currentCover = null;
        foreach ($currentFiles as $f) {
            if ($f['file_type'] === 'cover') {
                $currentCover = $f;
                break;
            }
        }

        $orientations  = $this->db->getAllOrientations();
        $apPrograms    = $this->db->getAllAPPrograms();
        $finalityTypes = $this->db->getAllFinalityTypes();
        $languages     = $this->db->getPredefinedLanguages();
        $formatTypes   = $this->db->getAllFormatTypes();
        $licenseTypes        = $this->db->getAllLicenseTypes();
        $enabledAccessTypes  = $this->db->getEnabledFormAccessTypes();

        $rawRow              = $this->db->getThesisRawFields($thesisId);
        $currentLicenseId    = $rawRow['license_id']    ?? null;
        $currentAccessTypeId = $rawRow['access_type_id'] ?? null;
        $currentContextNote  = $rawRow['context_note']   ?? '';

        // Author contact info (from view)
        $contactInterne       = $thesis['contact_interne'] ?? '';
        $contactPublic        = (bool)($thesis['contact_public'] ?? false);

        return [
            'thesis'                    => $thesis,
            'currentLanguages'          => $currentLanguages,
            'currentFormats'            => $currentFormats,
            'jury'                      => $jury,
            'currentFiles'              => $currentFiles,
            'currentCover'              => $currentCover,
            'orientations'              => $orientations,
            'apPrograms'                => $apPrograms,
            'finalityTypes'             => $finalityTypes,
            'languages'                 => $languages,
            'formatTypes'               => $formatTypes,
            'licenseTypes'              => $licenseTypes,
            'enabledAccessTypes'        => $enabledAccessTypes,
            'currentLicenseId'          => $currentLicenseId,
            'currentAccessTypeId'       => $currentAccessTypeId,
            'currentContextNote'        => $currentContextNote,
            'contactInterne' => $contactInterne,
            'contactPublic'  => $contactPublic,
            'currentRaw'                => $rawRow,
            'pageTitle'                 => 'Éditer TFE - ' . htmlspecialchars($thesis['title']),
        ];
    }

    // ── Write / action ────────────────────────────────────────────────────────

    /**
     * Validate and persist a thesis-edit POST submission.
     *
     * Runs the full update inside a transaction:
     *   1. Thesis metadata (title, subtitle, year, orientation, ap, finality,
     *      synopsis, context_note, baiu_link, license_id,
     *      access_type_id, is_published)
     *   2. Authors (setThesisAuthors)
     *   3. Jury   (setThesisJury)
     *   4. Languages (setThesisLanguages)
     *   5. Formats   (setThesisFormats)
     *   6. Tags      (setThesisTags)
     * Then handles banner upload/removal outside the transaction.
     *
     * @param int   $thesisId  Validated thesis ID (> 0).
     * @param array $post      Sanitised $_POST array.
     * @param array $files     $_FILES array (expects 'banner' key).
     *
     * @throws Exception on validation or DB error (caller must rollback if
     *                   the transaction is still open, but this method rolls
     *                   back internally before re-throwing).
     */
    public function save(int $thesisId, array $post, array $files): void
    {
        if ($thesisId <= 0) {
            throw new InvalidArgumentException('ID de TFE invalide.');
        }

        // ── Basic validation (same required fields as create) ──────────────────
        $errors = [];
        $titre = trim($post['titre'] ?? '');
        if ($titre === '') {
            $errors[] = 'Le titre est requis.';
        }
        $auteurice = trim($post['auteurice'] ?? '');
        if ($auteurice === '') {
            $errors[] = "L'auteur·ice est requis.";
        }
        $synopsis = trim($post['synopsis'] ?? '');
        if ($synopsis === '') {
            $errors[] = 'Le synopsis est requis.';
        }
        $annee = intval($post['année'] ?? 0);
        if ($annee < 2000 || $annee > ((int)date('Y') + 1)) {
            $errors[] = "L'année est invalide.";
        }
        $orientationId = intval($post['orientation'] ?? 0);
        $apProgramId = intval($post['ap'] ?? 0);
        $finalityId = intval($post['finality'] ?? 0);

        if (!empty($errors)) {
            throw new RuntimeException(implode(' ', $errors));
        }

        $this->db->beginTransaction();

        try {
            // ── 1. Thesis metadata ────────────────────────────────────────────
            $meta = [
                'title'           => trim($post['titre']          ?? ''),
                'subtitle'        => trim($post['subtitle']       ?? ''),
                'year'            => intval($post['année']        ?? 0),
                'orientation_id'  => ($v = intval($post['orientation'] ?? 0)) > 0 ? $v : null,
                'ap_program_id'   => ($v = intval($post['ap']          ?? 0)) > 0 ? $v : null,
                'finality_id'     => ($v = intval($post['finality']    ?? 0)) > 0 ? $v : null,
                'synopsis'        => trim($post['synopsis']       ?? ''),
                'context_note'    => trim($post['context_note']   ?? ''),
                'baiu_link'       => trim($post['lien']           ?? ''),
                'license_id'      => filter_var($post['license_id']    ?? '', FILTER_VALIDATE_INT) ?: null,
                'access_type_id'  => filter_var($post['access_type_id'] ?? '', FILTER_VALIDATE_INT) ?: null,
                'is_published'    => isset($post['is_published']),
                'remarks'         => trim($post['remarks']          ?? ''),
                'jury_points'     => $post['jury_points'] ?? null,
                'exemplaire_baiu' => !empty($post['exemplaire_baiu']),
                'exemplaire_erg'  => !empty($post['exemplaire_erg']),
                'cc2r'            => !empty($post['cc2r']),
                'license_custom'  => trim($post['license_custom'] ?? ''),
            ];
            $this->db->updateThesis($thesisId, $meta);
            error_log('[ThesisEdit] Step 1 OK — thesis_id=' . $thesisId);

            // ── 2. Authors (alphabetically sorted) ─────────────────────────────
            $authorsRaw    = trim($post['auteurice'] ?? '');
            $showContact   = !empty($post['contact_public']);
            // contact_interne (backoffice) takes precedence over mail (tfe-info fieldset)
            $contactInterne = trim($post['contact_interne'] ?? '');
            $firstAuthorEmail = $contactInterne !== '' ? $contactInterne : ($post['mail'] ?? null);
            $authorNames   = [];
            if ($authorsRaw !== '') {
                $authorNames = array_values(array_filter(array_map('trim', explode(',', $authorsRaw)), fn ($n) => $n !== ''));
                sort($authorNames, SORT_NATURAL);
            }
            $authorEntries = [];
            foreach ($authorNames as $i => $name) {
                $authorEntries[] = [
                    'name'         => $name,
                    'email'        => $i === 0 ? $firstAuthorEmail : null,
                    'show_contact' => $i === 0 ? $showContact : false,
                ];
            }
            $this->db->setThesisAuthors($thesisId, $authorEntries);
            error_log('[ThesisEdit] Step 2 OK — authors=' . json_encode($authorNames));

            // ── 3. Jury ───────────────────────────────────────────────────────
            $juryMembers = $this->collectJuryMembers($post);
            $this->db->setThesisJury($thesisId, $juryMembers);
            error_log('[ThesisEdit] Step 3 OK — jury=' . count($juryMembers));

            // ── 4. Languages ──────────────────────────────────────────────────
            $langIds = isset($post['languages']) && is_array($post['languages'])
                ? $post['languages']
                : [];
            // language_autre: pill-based component sends an array; also handle legacy comma-separated string
            $autreRaw = $post['language_autre'] ?? '';
            if (is_array($autreRaw)) {
                foreach ($autreRaw as $langName) {
                    $langName = trim($langName);
                    if ($langName !== '') {
                        $langIds[] = (string)$this->db->getOrCreateLanguage($langName);
                    }
                }
            } elseif (is_string($autreRaw) && trim($autreRaw) !== '') {
                foreach (array_map('trim', explode(',', $autreRaw)) as $langName) {
                    if ($langName !== '') {
                        $langIds[] = (string)$this->db->getOrCreateLanguage($langName);
                    }
                }
            }
            $this->db->setThesisLanguages($thesisId, $langIds);
            error_log('[ThesisEdit] Step 4 OK — languages=' . json_encode($langIds));

            // ── 5. Formats ────────────────────────────────────────────────────
            $formatIds = isset($post['formats']) && is_array($post['formats'])
                ? $post['formats']
                : [];
            $this->db->setThesisFormats($thesisId, $formatIds);
            error_log('[ThesisEdit] Step 5 OK — formats=' . json_encode($formatIds));

            // ── 6. Tags ───────────────────────────────────────────────────────
            $normalizeTag = fn(string $t): string => strtolower(trim(preg_replace('/\s+/', ' ', $t)));
            $keywords = [];
            if (isset($post['tag']) && is_array($post['tag'])) {
                $keywords = array_values(array_unique(array_map(
                    $normalizeTag,
                    array_map(fn($t) => (string)$t, $post['tag'])
                )));
            } else {
                $keywordsRaw = trim($post['tag'] ?? '');
                if ($keywordsRaw !== '') {
                    $keywords = array_map($normalizeTag, explode(',', $keywordsRaw));
                }
            }
            $keywords = array_values(array_unique($keywords));
            $keywords = array_filter($keywords, fn($t) => $t !== '');
            $keywords = array_slice($keywords, 0, 10);
            if (count($keywords) < 1) {
                throw new Exception('Veuillez indiquer au moins 1 mot-clé.');
            }
            $this->db->setThesisTags($thesisId, $keywords);
            error_log('[ThesisEdit] Step 6 OK — tags=' . json_encode($keywords));

            $this->db->commit();
            error_log('[ThesisEdit] COMMIT OK — thesis_id=' . $thesisId);

        } catch (Exception $e) {
            ErrorHandler::log('thesis_edit_tx', $e, ['thesis_id' => $thesisId]);
            $this->db->rollback();
            throw $e;
        }

        // ── Resolve thesis folder path (reuse existing or build new) ────────
        $year    = intval($post['année'] ?? date('Y'));
        $title   = trim($post['titre'] ?? '');
        $authors = trim($post['auteurice'] ?? '');

        $tf = $this->buildThesisFolder($year, $authors, $title);
        $folderPath = $tf['folderPath'];
        $filePrefix = $tf['filePrefix'];

        // Reuse existing folder if this thesis already has files on disk
        $existingFiles = $this->db->getThesisFiles($thesisId);
        foreach ($existingFiles as $f) {
            $fp = $f['file_path'] ?? '';
            if (str_starts_with($fp, 'theses/')) {
                $parts = explode('/', $fp);
                if (count($parts) >= 3) {
                    $folderName = $parts[2];
                    $folderPath = 'theses/' . $year . '/' . $folderName . '/';
                    $filePrefix = $folderName;
                    break;
                }
            }
        }

        // Ensure the folder exists
        $dirAbs = STORAGE_ROOT . '/' . $folderPath;
        if (!is_dir($dirAbs)) {
            mkdir($dirAbs, 0755, true);
        }

        // ── Cover image (outside transaction — filesystem op) ─────────────────
        if (isset($post['remove_cover'])) {
            foreach ($existingFiles as $f) {
                if ($f['file_type'] === 'cover') {
                    $this->db->deleteThesisFile((int)$f['id'], $thesisId);
                    if (!empty($f['file_path']) && defined('STORAGE_ROOT')) {
                        $abs = STORAGE_ROOT . '/' . $f['file_path'];
                        if (file_exists($abs)) {
                            @unlink($abs);
                        }
                    }
                    break;
                }
            }
        } else {
            $this->handleCoverUpload($thesisId, $files['couverture'] ?? null, $folderPath, $filePrefix);
        }

        // ── Note d'intention (replace if uploaded) ────────────────────────────
        // Remove old note_intention row+file if new one is uploaded
        if (!empty($files['note_intention']['tmp_name'] ?? null) && ($files['note_intention']['error'] ?? -1) === UPLOAD_ERR_OK) {
            foreach ($existingFiles as $f) {
                if ($f['file_type'] === 'note_intention') {
                    $this->db->deleteThesisFile((int)$f['id'], $thesisId);
                    if (!empty($f['file_path']) && defined('STORAGE_ROOT')) {
                        $abs = STORAGE_ROOT . '/' . $f['file_path'];
                        if (file_exists($abs)) {
                            @unlink($abs);
                        }
                    }
                    break;
                }
            }
        }
        $this->handleNoteIntentionUpload($thesisId, $files['note_intention'] ?? null, $folderPath, $filePrefix);

        // ── Delete individual thesis files ────────────────────────────────────
        $deleteIds = isset($post['delete_files']) && is_array($post['delete_files'])
            ? array_map('intval', $post['delete_files'])
            : [];
        foreach ($deleteIds as $fileId) {
            if ($fileId <= 0) {
                continue;
            }
            $filePath = $this->db->deleteThesisFile($fileId, $thesisId);
            if ($filePath && defined('STORAGE_ROOT')) {
                // Skip filesystem deletion for website URLs (not real files)
                if (!str_starts_with($filePath, 'http://') && !str_starts_with($filePath, 'https://')) {
                    $abs = STORAGE_ROOT . '/' . $filePath;
                    if (file_exists($abs)) {
                        @unlink($abs);
                    }
                }
            }
        }

        // ── Reorder existing files ────────────────────────────────────────────
        if (!empty($post['file_sort_order']) && is_array($post['file_sort_order'])) {
            $this->db->reorderThesisFiles($thesisId, $post['file_sort_order']);
        }

        // ── Update display labels for existing files ──────────────────────────
        if (!empty($post['file_label']) && is_array($post['file_label'])) {
            foreach ($post['file_label'] as $fileId => $label) {
                $fileId = (int)$fileId;
                if ($fileId <= 0) {
                    continue;
                }
                $this->db->updateThesisFileLabel($fileId, $thesisId, trim($label) ?: null);
            }
        }

        // ── New TFE/video/audio files upload (from client-side JS queue) ──
        $queueFiles = $files['queue_file'] ?? [];
        $qTfe    = $this->extractFilesSubArray($queueFiles, 'tfe');
        $qVideo  = $this->extractFilesSubArray($queueFiles, 'video');
        $qAudio  = $this->extractFilesSubArray($queueFiles, 'audio');
        $qAnnexe = $this->extractFilesSubArray($queueFiles, 'annexe');

        $tfeCount = 0;
        foreach ($existingFiles as $f) {
            if (!in_array($f['file_type'] ?? '', ['cover', 'note_intention', 'website', 'annex', 'caption'], true)
                && !str_starts_with($f['file_path'] ?? '', 'http')) {
                $tfeCount++;
            }
        }

        $startNum = $tfeCount + 1;
        $startNum = $this->handleTfeQueueFiles($thesisId, $qTfe, $folderPath, $filePrefix, $startNum);
        $startNum = $this->handleTfeQueueFiles($thesisId, $qVideo, $folderPath, $filePrefix, $startNum);
        $this->handleTfeQueueFiles($thesisId, $qAudio, $folderPath, $filePrefix, $startNum);
        $this->handleAnnexeQueueFiles($thesisId, $qAnnexe, $folderPath, $filePrefix);

        // Legacy annexe files (direct upload, non-queue path — kept for backwards compat)
        if (isset($files['annexes']) && is_array($files['annexes']['name'] ?? null)) {
            $this->handleAnnexeFiles($thesisId, $files['annexes'], $folderPath, $filePrefix, $post);
        }

        // ── PeerTube video / audio uploads ────────────────────────────────────
        $this->handlePeerTubeUpload($thesisId, trim($post['titre'] ?? ''), $files, 'peertube_video');
        $this->handlePeerTubeUpload($thesisId, trim($post['titre'] ?? ''), $files, 'peertube_audio');

        // ── Website URL — add or update ──────────────────────────────────────
        $this->handleWebsiteUrl($thesisId, $post);
    }

    // ── WCAG 3.3.1 helper ─────────────────────────────────────────────────────

    /**
     * Map a validation exception message to the name of the field that should
     * receive autofocus when the form is re-rendered.
     *
     * Returns null when no field mapping is found.
     */
    public static function autofocusFieldForError(string $message): ?string
    {
        if (str_contains($message, 'titre')    || str_contains($message, 'Titre')) {
            return 'titre';
        }
        if (str_contains($message, 'année')    || str_contains($message, 'Année')) {
            return 'année';
        }
        if (str_contains($message, 'synopsis') || str_contains($message, 'Synopsis')) {
            return 'synopsis';
        }
        if (str_contains($message, 'auteur')   || str_contains($message, 'Auteur')) {
            return 'auteurice';
        }
        if (str_contains($message, 'orientation')) {
            return 'orientation';
        }
        if (str_contains($message, 'atelier')) {
            return 'ap';
        }
        if (str_contains($message, 'finalité')) {
            return 'finality';
        }
        if (str_contains($message, 'langue')) {
            return 'languages';
        }
        if (str_contains($message, 'format')) {
            return 'formats';
        }
        if (str_contains($message, 'licence')) {
            return 'license_id';
        }
        if (str_contains($message, 'promoteur')) {
            return 'jury_promoteur';
        }
        if (str_contains($message, 'lecteur·ice interne')) {
            return 'jury_lecteur_interne[]';
        }
        if (str_contains($message, 'lecteur·ice externe')) {
            return 'jury_lecteur_externe[]';
        }
        return null;
    }

    // ── Private helpers ───────────────────────────────────────────────────────

    /**
     * Build the jury-members array from POST data.
     *
     * @param  array $post  Raw $_POST.
     * @return array<int, array{name: string, role: string, is_external: int}>
     */
    private function collectJuryMembers(array $post): array
    {
        $members = [];

        // Promoteurs internes (accept both scalar and array)
        $promoteurs = $post['jury_promoteur'] ?? null;
        if ($promoteurs !== null && !is_array($promoteurs)) {
            $promoteurs = [$promoteurs];
        }
        if (is_array($promoteurs)) {
            foreach ($promoteurs as $name) {
                $name = trim($name ?? '');
                if ($name !== '') {
                    $members[] = ['name' => $name, 'role' => 'promoteur', 'is_external' => 0, 'is_ulb' => 0];
                }
            }
        }

        // Promoteurs ULB (accept both scalar and array)
        $promoteursUlb = $post['jury_promoteur_ulb_name'] ?? null;
        if ($promoteursUlb !== null && !is_array($promoteursUlb)) {
            $promoteursUlb = [$promoteursUlb];
        }
        if (is_array($promoteursUlb)) {
            foreach ($promoteursUlb as $name) {
                $name = trim($name ?? '');
                if ($name !== '') {
                    $members[] = ['name' => $name, 'role' => 'promoteur', 'is_external' => 1, 'is_ulb' => 1];
                }
            }
        }

        // Lecteurs internes
        foreach ($post['jury_lecteur_interne'] ?? [] as $name) {
            $name = trim($name);
            if ($name !== '') {
                $members[] = ['name' => $name, 'role' => 'lecteur', 'is_external' => 0];
            }
        }

        // Lecteurs externes
        foreach ($post['jury_lecteur_externe'] ?? [] as $name) {
            $name = trim($name);
            if ($name !== '') {
                $members[] = ['name' => $name, 'role' => 'lecteur', 'is_external' => 1];
            }
        }

        // Backwards compat: old jury_lecteurs[]
        if (isset($post['jury_lecteurs'])) {
            foreach ($post['jury_lecteurs'] as $i => $name) {
                $name = trim($name);
                if ($name !== '') {
                    $members[] = [
                        'name'        => $name,
                        'role'        => 'lecteur',
                        'is_external' => isset($post['jury_lecteurs_ext'][$i]) ? 1 : 0,
                    ];
                }
            }
        }

        return $members;
    }

    /**
     * Upload a video or audio file to PeerTube when the feature is enabled.
     *
     * @param int    $thesisId   Thesis to attach the result to.
     * @param string $title      Title to use on PeerTube.
     * @param array  $files      $_FILES array.
     * @param string $inputName  'peertube_video' or 'peertube_audio'.
     */
    private function handlePeerTubeUpload(int $thesisId, string $title, array $files, string $inputName): void
    {
        $upload = $files[$inputName] ?? null;
        if (!$upload || !isset($upload['error']) || $upload['error'] !== UPLOAD_ERR_OK) {
            return;
        }

        require_once APP_ROOT . '/src/PeerTubeService.php';
        if (!PeerTubeService::isEnabled($this->db)) {
            return;
        }

        try {
            $watchUrl = PeerTubeService::upload(
                $this->db,
                $upload['tmp_name'],
                $title,
                ''
            );

            $fileType = str_contains($inputName, 'audio') ? 'audio' : 'video';
            $this->db->insertThesisFile(
                $thesisId,
                $fileType,
                $watchUrl,
                basename($upload['name']),
                $upload['size'],
                $upload['type'] ?? 'application/octet-stream',
                null,
                null
            );
            error_log("ThesisEditController: PeerTube upload OK → $watchUrl");
        } catch (\Throwable $e) {
            error_log('ThesisEditController: PeerTube upload failed — ' . $e->getMessage());
        }
    }

    /**
     * Add or update a website URL thesis_file row.
     *
     * If a website row already exists for this thesis, it is replaced.
     * Otherwise a new row is inserted.
     */
    private function handleWebsiteUrl(int $thesisId, array $post): void
    {
        $websiteUrl = trim($post['website_url'] ?? '');

        // Remove existing website rows
        $existingFiles = $this->db->getThesisFiles($thesisId);
        foreach ($existingFiles as $f) {
            if ($f['file_type'] === 'website') {
                $this->db->deleteThesisFile((int)$f['id'], $thesisId);
            }
        }

        if ($websiteUrl === '') {
            return;
        }

        // Validate URL
        $websiteUrl = filter_var($websiteUrl, FILTER_VALIDATE_URL);
        if ($websiteUrl === false) {
            error_log('ThesisEditController: invalid website URL, skipping');
            return;
        }

        $label     = trim($post['website_label'] ?? '');
        $sortOrder = isset($post['website_order']) ? (int)$post['website_order'] : null;
        $fileName  = rtrim(preg_replace('#^https?://#i', '', $websiteUrl), '/');

        $this->db->insertThesisFile(
            $thesisId,
            'website',
            $websiteUrl,
            $fileName,
            0,
            'text/html',
            $label !== '' ? $label : null,
            $sortOrder
        );
        error_log("ThesisEditController: website stored → $websiteUrl");
    }
}
