<?php
/**
 * ThesisCreateController
 *
 * Centralises all validation, data-fetching, and persistence logic for the
 * admin "add new thesis" workflow (admin/add.php + admin/actions/formulaire.php).
 *
 * Responsibilities:
 *  - Loading lookup tables for the add-form view (loadFormData)
 *  - Validating and sanitising POST submissions
 *  - Creating the thesis record, linking authors / jury / languages / formats /
 *    tags in a single database transaction
 *  - Handling cover image, banner, and multi-file uploads
 *  - WCAG 3.3.1: mapping validation exception messages to autofocus field hints
 *
 * The class has NO output side-effects; all redirects, flash writes, session
 * mutations, and template rendering stay in the thin dispatcher files so the
 * view layer remains easy to inspect and modify.
 */
class ThesisCreateController
{
    /** Maximum allowed file size for thesis files (bytes). */
    private const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50 MB

    /** MIME types accepted for thesis files. */
    private const ALLOWED_MIME_TYPES = [
        'image/jpeg', 'image/png', 'application/pdf',
        'video/mp4', 'application/zip', 'text/vtt',
    ];

    /** File extensions accepted for thesis files. */
    private const ALLOWED_EXTENSIONS = ['jpg', 'jpeg', 'png', 'pdf', 'mp4', 'zip', 'vtt'];

    private Database $db;

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

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

    /**
     * Convenience factory — instantiates Database and returns a ready
     * controller instance.
     */
    public static function make(): self
    {
        require_once APP_ROOT . '/src/Database.php';

        return new self(new Database());
    }

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

    /**
     * Load all lookup tables required to render the add-thesis form.
     *
     * Returns a flat array of view variables:
     *   - 'orientations'  – orientation lookup rows
     *   - 'apPrograms'    – AP program lookup rows
     *   - 'finalityTypes' – finality type lookup rows
     *   - 'languages'     – language lookup rows
     *   - 'formatTypes'   – format type lookup rows
     *   - 'licenseTypes'  – license type lookup rows
     *
     * @return array<string, mixed>
     * @throws Exception on DB error.
     */
    public function loadFormData(): array
    {
        return [
            'orientations'        => $this->db->getAllOrientations(),
            'apPrograms'          => $this->db->getAllAPPrograms(),
            'finalityTypes'       => $this->db->getAllFinalityTypes(),
            'languages'           => $this->db->getAllLanguages(),
            'formatTypes'         => $this->db->getAllFormatTypes(),
            'licenseTypes'        => $this->db->getAllLicenseTypes(),
            'enabledAccessTypes'  => $this->db->getEnabledFormAccessTypes(),
        ];
    }

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

    /**
     * Validate and persist a new-thesis POST submission.
     *
     * On success, returns the new thesis ID so the caller can redirect to
     * thanks.php?id=<n>.  On validation or DB failure, throws an Exception
     * (caller must flash the message and redirect back to the form).
     *
     * Execution order:
     *   1. Validate + sanitise POST fields
     *   2. Find/create author record
     *   3. INSERT thesis row + link author (inside transaction)
     *   4. Link jury, languages, formats, tags (inside transaction)
     *   5. COMMIT
     *   6. Handle file uploads: cover, banner, thesis files (outside transaction)
     *
     * @param  array $post   Sanitised $_POST array.
     * @param  array $files  $_FILES array.
     * @return int           The newly created thesis ID.
     * @throws Exception     On validation or DB error.
     */
    public function submit(array $post, array $files): int
    {
        // ── 1. Validate + sanitise ────────────────────────────────────────────
        $data = $this->validateAndSanitise($post);

        // ── 2. Find / create author ───────────────────────────────────────────
        $authorId = $this->db->findOrCreateAuthor($data['auteurName'], $data['mail'] ?: null, $data['showContact']);
        error_log("ThesisCreateController: author ID $authorId");

        // ── 3–4. DB writes in a transaction ───────────────────────────────────
        $this->db->beginTransaction();

        try {
            $thesisId = $this->db->createThesis([
                'year'           => $data['annee'],
                'orientation_id' => $data['orientationId'],
                'ap_program_id'  => $data['apProgramId'],
                'finality_id'    => $data['finalityId'],
                'title'          => $data['titre'],
                'subtitle'       => $data['subtitle'],
                'synopsis'       => $data['synopsis'],
                'file_size_info' => $data['durationInfo'],
                'baiu_link'      => $data['lien'],
                'license_id'     => $data['licenseId'],
                'access_type_id' => $data['accessTypeId'],
                'author_id'      => $authorId,
            ]);

            $identifier = $this->db->getThesisIdentifier($thesisId);
            error_log("ThesisCreateController: created thesis #$thesisId ($identifier)");

            $this->db->setThesisJury($thesisId, $data['juryMembers']);
            $this->db->setThesisLanguages($thesisId, $data['languageIds']);
            $this->db->setThesisFormats($thesisId, $data['formatIds']);
            $this->db->setThesisTags($thesisId, $data['keywords']);

            $this->db->commit();

        } catch (Exception $e) {
            $this->db->rollback();
            throw $e;
        }

        // ── 5. File uploads (outside transaction — filesystem ops) ────────────
        $this->handleCoverUpload($thesisId, $files['couverture'] ?? null);
        $this->db->handleBannerUpload($thesisId, $files['banner'] ?? null);
        $this->handleThesisFiles($thesisId, $data['annee'], $identifier, $files['files'] ?? null);

        return $thesisId;
    }

    // ── 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 after an error.
     *
     * Returns null when no field mapping is found.
     */
    public static function autofocusFieldForError(string $message): ?string
    {
        if (str_contains($message, 'Nom/Prénom/Pseudo'))    return 'auteurice';
        if (str_contains($message, 'Titre du mémoire'))     return 'titre';
        if (str_contains($message, 'Synopsis'))             return 'synopsis';
        if (str_contains($message, 'Année invalide'))       return 'année';
        if (str_contains($message, 'orientation'))          return 'orientation';
        if (str_contains($message, 'Atelier Pratique'))     return 'ap';
        if (str_contains($message, 'finalité'))             return 'finality';
        if (str_contains($message, 'langue'))               return 'languages';
        if (str_contains($message, 'mots-clés'))            return 'tag';
        if (str_contains($message, 'Lien URL'))             return 'lien';
        if (str_contains($message, 'e-mail de confirmation')) return 'confirmation_email';
        return null;
    }

    // ── Private: validation ───────────────────────────────────────────────────

    /**
     * Validate and sanitise all POST fields for a new thesis submission.
     *
     * Returns a flat associative array of clean values.
     *
     * @param  array $post Raw $_POST.
     * @return array<string, mixed>
     * @throws Exception on validation failure.
     */
    private function validateAndSanitise(array $post): array
    {
        $auteurName = $this->validateRequired(
            $this->sanitiseString($post['auteurice'] ?? ''),
            'Nom/Prénom/Pseudo'
        );

        $mail        = !empty($post['mail']) ? $this->sanitiseString($post['mail']) : '';
        $showContact = !empty($post['contact_public']) ? true : false;

        $annee = filter_var($post['année'] ?? '', FILTER_VALIDATE_INT);
        if ($annee === false || $annee < 2000 || $annee > ((int) date('Y') + 1)) {
            throw new Exception('Année invalide. Veuillez entrer une année valide.');
        }

        $orientationId = filter_var($post['orientation'] ?? '', FILTER_VALIDATE_INT);
        if ($orientationId === false) {
            throw new Exception('Veuillez sélectionner une orientation.');
        }

        $apProgramId = filter_var($post['ap'] ?? '', FILTER_VALIDATE_INT);
        if ($apProgramId === false) {
            throw new Exception('Veuillez sélectionner un Atelier Pratique.');
        }

        $finalityId = filter_var($post['finality'] ?? '', FILTER_VALIDATE_INT);
        if ($finalityId === false) {
            throw new Exception('Veuillez sélectionner une finalité.');
        }

        $titre    = $this->validateRequired($this->sanitiseString($post['titre'] ?? ''), 'Titre du mémoire');
        $subtitle = $this->sanitiseString($post['subtitle'] ?? '');
        $synopsis = $this->validateRequired($this->sanitiseString($post['synopsis'] ?? ''), 'Synopsis');

        $durationInfo = $this->sanitiseString($post['duration_info'] ?? '');

        // Jury members
        $juryMembers = [];
        if (!empty(trim($post['jury_president'] ?? ''))) {
            $juryMembers[] = ['name' => trim($post['jury_president']), 'role' => 'president', 'is_external' => 0];
        }
        if (!empty(trim($post['jury_promoteur'] ?? ''))) {
            $juryMembers[] = [
                'name'        => trim($post['jury_promoteur']),
                'role'        => 'promoteur',
                'is_external' => isset($post['jury_promoteur_ext']) ? 1 : 0,
            ];
        }
        foreach ($post['jury_lecteurs'] ?? [] as $i => $name) {
            $name = trim($name);
            if ($name !== '') {
                $juryMembers[] = [
                    'name'        => $name,
                    'role'        => 'lecteur',
                    'is_external' => isset($post['jury_lecteurs_ext'][$i]) ? 1 : 0,
                ];
            }
        }

        // Keywords (max 10)
        $tagRaw   = $this->sanitiseString($post['tag'] ?? '');
        $keywords = $tagRaw !== '' ? array_map('trim', explode(',', $tagRaw)) : [];
        if (count($keywords) > 10) {
            throw new Exception('Maximum 10 mots-clés autorisés.');
        }

        // Languages (at least one required)
        $languageIds = isset($post['languages']) && is_array($post['languages'])
            ? array_map('intval', $post['languages'])
            : [];
        if (empty($languageIds)) {
            throw new Exception('Veuillez sélectionner au moins une langue.');
        }

        // Formats (optional)
        $formatIds = isset($post['formats']) && is_array($post['formats'])
            ? array_map('intval', $post['formats'])
            : [];

        $licenseId = filter_var($post['license_id'] ?? '', FILTER_VALIDATE_INT) ?: null;

        // Access type — must be one of the enabled types; default 2 (Interne)
        $accessTypeId = filter_var($post['access_type_id'] ?? '', FILTER_VALIDATE_INT);
        if ($accessTypeId === false || $accessTypeId <= 0) {
            $accessTypeId = 2; // Interne
        }

        // External link (optional)
        $lien = '';
        if (!empty($post['lien'])) {
            $lien = filter_var($post['lien'], FILTER_VALIDATE_URL);
            if ($lien === false) {
                throw new Exception('Lien URL invalide.');
            }
        }

        // Confirmation e-mail (required)
        $confirmationEmail = trim($post['confirmation_email'] ?? '');
        if ($confirmationEmail === '') {
            throw new Exception("L'adresse e-mail de confirmation est requise.");
        }
        $confirmationEmail = filter_var($confirmationEmail, FILTER_VALIDATE_EMAIL);
        if ($confirmationEmail === false) {
            throw new Exception("L'adresse e-mail de confirmation n'est pas valide.");
        }

        return compact(
            'auteurName', 'mail', 'showContact', 'confirmationEmail', 'annee', 'orientationId', 'apProgramId',
            'finalityId', 'titre', 'subtitle', 'synopsis', 'durationInfo',
            'juryMembers', 'keywords', 'languageIds', 'formatIds',
            'licenseId', 'lien', 'accessTypeId'
        );
    }

    // ── Private: file uploads ─────────────────────────────────────────────────

    /**
     * Process an optional cover image upload and record it in thesis_files.
     *
     * @param int        $thesisId
     * @param array|null $upload   Single-file $_FILES entry (may be null or have UPLOAD_ERR_NO_FILE).
     */
    private function handleCoverUpload(int $thesisId, ?array $upload): void
    {
        if (!$upload || !isset($upload['error']) || $upload['error'] !== UPLOAD_ERR_OK) {
            return;
        }

        $finfo     = new finfo(FILEINFO_MIME_TYPE);
        $mimeType  = $finfo->file($upload['tmp_name']);
        $ext       = strtolower(pathinfo($upload['name'], PATHINFO_EXTENSION));

        if (!in_array($mimeType, ['image/jpeg', 'image/png'], true)
            || !in_array($ext, ['jpg', 'jpeg', 'png'], true)) {
            error_log("ThesisCreateController: invalid cover MIME $mimeType, skipping");
            return;
        }

        $coverDir = STORAGE_ROOT . '/covers/';
        if (!is_dir($coverDir)) {
            mkdir($coverDir, 0755, true);
        }

        $safeName   = bin2hex(random_bytes(16)) . '.' . $ext;
        $targetPath = $coverDir . $safeName;

        if (!move_uploaded_file($upload['tmp_name'], $targetPath)) {
            error_log("ThesisCreateController: failed to move cover to $targetPath");
            return;
        }

        chmod($targetPath, 0644);
        $relPath = 'covers/' . $safeName;

        $this->db->insertThesisFile($thesisId, 'cover', $relPath, basename($upload['name']), $upload['size'], $mimeType);
        error_log("ThesisCreateController: cover uploaded → $safeName");
    }

    /**
     * Process multiple thesis-file uploads (PDFs, images, videos, ZIPs, VTTs).
     *
     * @param int        $thesisId
     * @param int        $year        Used for the storage sub-directory path.
     * @param string     $identifier  Thesis identifier slug (e.g. "2024-003").
     * @param array|null $uploads     Multi-file $_FILES entry (may be null).
     */
    private function handleThesisFiles(int $thesisId, int $year, string $identifier, ?array $uploads): void
    {
        if (!$uploads || !is_array($uploads['name'] ?? null)) {
            return;
        }

        $uploadDir = STORAGE_ROOT . "/theses/{$year}/{$identifier}/";
        if (!is_dir($uploadDir)) {
            mkdir($uploadDir, 0755, true);
        }

        $count = count($uploads['name']);
        for ($i = 0; $i < $count; $i++) {
            if (($uploads['error'][$i] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_NO_FILE) {
                continue;
            }

            if (($uploads['error'][$i] ?? -1) !== UPLOAD_ERR_OK) {
                error_log("ThesisCreateController: upload error code {$uploads['error'][$i]} for {$uploads['name'][$i]}");
                continue;
            }

            $finfo    = new finfo(FILEINFO_MIME_TYPE);
            $mimeType = $finfo->file($uploads['tmp_name'][$i]);
            $ext      = strtolower(pathinfo($uploads['name'][$i], PATHINFO_EXTENSION));

            // finfo may return 'text/plain' for WebVTT on some systems.
            if ($mimeType === 'text/plain' && $ext === 'vtt') {
                $mimeType = 'text/vtt';
            }

            if (!in_array($mimeType, self::ALLOWED_MIME_TYPES, true)
                || !in_array($ext, self::ALLOWED_EXTENSIONS, true)) {
                error_log("ThesisCreateController: invalid file type {$uploads['name'][$i]} ($mimeType), skipping");
                continue;
            }

            if ($uploads['size'][$i] > self::MAX_FILE_SIZE) {
                error_log("ThesisCreateController: file too large {$uploads['name'][$i]}, skipping");
                continue;
            }

            $safeName   = bin2hex(random_bytes(16)) . '.' . $ext;
            $targetPath = $uploadDir . $safeName;

            if (!move_uploaded_file($uploads['tmp_name'][$i], $targetPath)) {
                error_log("ThesisCreateController: failed to move file {$uploads['name'][$i]}");
                continue;
            }

            chmod($targetPath, 0644);

            $fileType = 'other';
            if ($ext === 'vtt') {
                $fileType = 'caption';
            } elseif (stripos($uploads['name'][$i], 'annex') !== false) {
                $fileType = 'annex';
            } elseif ($ext === 'pdf') {
                $fileType = 'main';
            }

            $relPath = "theses/{$year}/{$identifier}/" . $safeName;
            $this->db->insertThesisFile(
                $thesisId,
                $fileType,
                $relPath,
                basename($uploads['name'][$i]),
                $uploads['size'][$i],
                $mimeType
            );
            error_log("ThesisCreateController: file uploaded → $safeName ($fileType)");
        }
    }

    // ── Private: input helpers ────────────────────────────────────────────────

    /**
     * Trim and strip HTML tags from a string value.
     * htmlspecialchars is applied at render time, not here.
     */
    private function sanitiseString(string $input): string
    {
        return strip_tags(trim($input));
    }

    /**
     * Assert that a string value is non-empty.
     *
     * @throws Exception if $value is empty.
     */
    private function validateRequired(string $value, string $fieldName): string
    {
        if ($value === '') {
            throw new Exception("Le champ '$fieldName' est requis.");
        }

        return $value;
    }
}
