<?php

declare(strict_types=1);

namespace App\Controllers;

use App\Models\AcademicInfo;
use App\Models\Admission;
use App\Models\Attachment;
use App\Models\Dependent;
use App\Support\AdmissionAttachments;
use App\Support\AdmissionForm;
use Core\Controller;
use Core\Csrf;
use Core\Database;
use Core\RequestLimits;
use Core\Session;
use Core\Upload;
use Core\Validator;
use PDOException;
use RuntimeException;
use Throwable;

final class AdmissionController extends Controller
{
    private const PENDING_PROFILE_PHOTO_SESSION_KEY = 'pending_profile_photo_admission_id';

    public function create(): void
    {
        $config = require dirname(__DIR__, 2) . '/config/app.php';

        $this->view('admissions/create', [
            'config' => $config,
            'errors' => Session::errors(),
            'error' => Session::getFlash('error'),
            'formTimeoutSeconds' => RequestLimits::formTimeoutSeconds(),
            'maxUploadFileBytes' => RequestLimits::uploadMaxFileBytes(),
            'maxUploadFileLabel' => RequestLimits::uploadMaxFileLabel(),
            'postMaxBytes' => RequestLimits::postMaxBytes(),
            'postMaxLabel' => RequestLimits::postMaxLabel(),
        ]);
    }

    public function store(): void
    {
        if ($this->isOversizedSubmission()) {
            Session::flash('error', 'O envio falhou porque o total dos anexos passou do limite do PHP (' . RequestLimits::postMaxLabel() . '). Reduza o tamanho dos arquivos e tente novamente.');
            $this->redirectTo('');
        }

        if ($this->isExpiredSubmission()) {
            $timeoutMinutes = max(1, (int) ceil(RequestLimits::formTimeoutSeconds() / 60));
            Session::flash('error', 'O tempo máximo do formulário é de ' . $timeoutMinutes . ' minuto(s). A ficha foi reiniciada; preencha novamente e envie dentro desse prazo.');
            $this->redirectTo('');
        }

        if (!Csrf::verify($_POST['_token'] ?? null)) {
            Session::flash('error', 'A sessão do formulário expirou ou o token ficou inválido. Reabra a ficha e tente novamente.');
            $this->redirectTo('');
        }

        $errors = array_merge(
            Validator::admission($_POST, $_FILES, false),
            AdmissionAttachments::validate($_POST, $_FILES)
        );

        $cpf = AdmissionForm::normalizeDigits($_POST['cpf'] ?? '');
        if ($cpf !== '' && Admission::existsByCpf($cpf)) {
            $errors['cpf'] = 'Já existe uma ficha cadastrada com este CPF.';
        }

        if ($errors !== []) {
            $this->failValidation($errors);
        }

        $storedPaths = [];
        $replacedPaths = [];
        $deletedPaths = [];
        $admissionId = null;

        try {
            Database::transaction(function () use (&$storedPaths, &$replacedPaths, &$deletedPaths, &$admissionId, $cpf): void {
                $admissionId = Admission::create(AdmissionForm::buildPublicPayload($_POST, $_SERVER, $cpf));

                Dependent::replaceForAdmission($admissionId, AdmissionForm::dependentEntries($_POST));
                AcademicInfo::replaceForAdmission(
                    $admissionId,
                    (int) ($_POST['is_student'] ?? 0) === 1 ? ($_POST['academic'] ?? null) : null
                );

                $sync = AdmissionAttachments::sync($admissionId, $_POST, $_FILES);
                $storedPaths = $sync['stored'];
                $replacedPaths = $sync['replaced'];
                $deletedPaths = $sync['deleted'];
            });
        } catch (PDOException $exception) {
            $this->cleanupStoredPaths($storedPaths);

            if (($exception->errorInfo[1] ?? null) === 1062) {
                $this->failValidation(['cpf' => 'Já existe uma ficha cadastrada com este CPF.']);
            }

            throw $exception;
        } catch (RuntimeException $exception) {
            $this->cleanupStoredPaths($storedPaths);
            $this->failValidation(['uploads' => $exception->getMessage()], 'Revise o formulário. Houve problema no envio dos anexos.');
        } catch (Throwable $exception) {
            $this->cleanupStoredPaths($storedPaths);
            throw $exception;
        }

        $this->cleanupStoredPaths(array_merge($replacedPaths, $deletedPaths));

        Session::clearOldInput();
        Session::clearErrors();
        Session::set(self::PENDING_PROFILE_PHOTO_SESSION_KEY, (int) $admissionId);
        Session::flash('success', 'Ficha enviada com sucesso. Falta só a foto de perfil, que agora é enviada separadamente para não pesar o formulário.');
        $this->redirectTo('admissoes/foto-perfil');
    }

    public function profilePhoto(): void
    {
        $admission = $this->pendingProfilePhotoAdmission();
        if ($admission === null) {
            Session::flash('error', 'Nenhuma ficha está aguardando envio de foto de perfil.');
            $this->redirectTo('');
        }

        $config = require dirname(__DIR__, 2) . '/config/app.php';
        $profilePhotoAttachment = Attachment::findByAdmissionAndLabel((int) $admission['id'], 'profile_photo');

        $this->view('admissions/profile-photo', [
            'config' => $config,
            'admission' => $admission,
            'profilePhotoAttachment' => $profilePhotoAttachment,
            'errors' => Session::errors(),
            'error' => Session::getFlash('error'),
            'success' => Session::getFlash('success'),
            'maxUploadFileBytes' => RequestLimits::uploadMaxFileBytes(),
            'maxUploadFileLabel' => RequestLimits::uploadMaxFileLabel(),
            'postMaxBytes' => RequestLimits::postMaxBytes(),
            'postMaxLabel' => RequestLimits::postMaxLabel(),
        ]);
    }

    public function storeProfilePhoto(): void
    {
        $admission = $this->pendingProfilePhotoAdmission();
        if ($admission === null) {
            Session::flash('error', 'A etapa de foto de perfil não está mais disponível. Reabra a ficha e tente novamente.');
            $this->redirectTo('');
        }

        if ($this->isOversizedSubmission()) {
            Session::flash('error', 'O envio da foto falhou porque o arquivo passou do limite do PHP (' . RequestLimits::postMaxLabel() . '). Reduza a imagem e tente novamente.');
            $this->redirectTo('admissoes/foto-perfil');
        }

        if (!Csrf::verify($_POST['_token'] ?? null)) {
            Session::flash('error', 'A sessão da foto de perfil expirou. Atualize a página e tente novamente.');
            $this->redirectTo('admissoes/foto-perfil');
        }

        $file = $_FILES['profile_photo'] ?? null;
        $errors = [];

        if (!is_array($file) || (int) ($file['error'] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_NO_FILE) {
            $errors['profile_photo'] = 'Selecione a foto de perfil para concluir esta etapa.';
        } else {
            $fileError = AdmissionAttachments::validateSingle($file);
            if ($fileError !== null) {
                $errors['profile_photo'] = $fileError;
            }
        }

        if ($errors !== []) {
            Session::flash('error', 'Revise a foto de perfil e tente novamente.');
            Session::putErrors($errors);
            $this->redirectTo('admissoes/foto-perfil');
        }

        $storedPath = null;
        $replacedPath = null;

        try {
            Database::transaction(function () use ($admission, $file, &$storedPath, &$replacedPath): void {
                $sync = AdmissionAttachments::syncSingle((int) $admission['id'], 'profile_photo', $file);
                $storedPath = $sync['stored'];
                $replacedPath = $sync['replaced'];
            });
        } catch (RuntimeException $exception) {
            $this->cleanupStoredPaths(array_filter([$storedPath]));
            Session::flash('error', $exception->getMessage());
            Session::putErrors(['profile_photo' => $exception->getMessage()]);
            $this->redirectTo('admissoes/foto-perfil');
        } catch (Throwable $exception) {
            $this->cleanupStoredPaths(array_filter([$storedPath]));
            throw $exception;
        }

        $this->cleanupStoredPaths(array_filter([$replacedPath]));

        Session::forget(self::PENDING_PROFILE_PHOTO_SESSION_KEY);
        Session::clearErrors();
        Session::flash('success', 'Ficha e foto de perfil enviadas com sucesso.');
        $this->redirectTo('obrigado');
    }

    public function success(): void
    {
        Session::forget(self::PENDING_PROFILE_PHOTO_SESSION_KEY);

        $this->view('admissions/success', ['message' => Session::getFlash('success', 'Ficha enviada com sucesso.')]);
    }

    private function pendingProfilePhotoAdmission(): ?array
    {
        $admissionId = (int) Session::get(self::PENDING_PROFILE_PHOTO_SESSION_KEY, 0);
        if ($admissionId <= 0) {
            return null;
        }

        return Admission::find($admissionId);
    }

    private function failValidation(array $errors, string $message = 'Revise o formulário. Existem campos obrigatórios ou inválidos.'): never
    {
        Session::flash('error', $message);
        Session::putErrors($errors);
        Session::putOldInput($_POST);
        $this->redirectTo('');
    }

    private function cleanupStoredPaths(array $relativePaths): void
    {
        foreach (array_unique(array_filter($relativePaths)) as $relativePath) {
            Upload::deleteRelativePath((string) $relativePath);
        }
    }

    private function isOversizedSubmission(): bool
    {
        $contentLength = (int) ($_SERVER['CONTENT_LENGTH'] ?? 0);
        if ($contentLength <= 0) {
            return false;
        }

        $postMaxBytes = RequestLimits::postMaxBytes();
        if ($postMaxBytes <= 0) {
            return false;
        }

        return $contentLength > $postMaxBytes;
    }

    private function isExpiredSubmission(): bool
    {
        $startedAt = filter_input(INPUT_POST, 'form_started_at', FILTER_VALIDATE_INT);
        if (!$startedAt) {
            return false;
        }

        return (time() - (int) $startedAt) > RequestLimits::formTimeoutSeconds();
    }
}
