<?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;
use function hash_hmac;

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

    public function create(): void
    {
        $pendingStepUrl = $this->pendingStepUrl();
        if ($pendingStepUrl !== null) {
            Session::clearOldInput();
            Session::clearErrors();
            redirect($pendingStepUrl);
        }

        $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 da ficha falhou porque a requisição passou do limite do PHP (' . RequestLimits::postMaxLabel() . '). 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 = Validator::admission($_POST, $_FILES, false, false);

        $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);
        }

        $admissionId = null;

        try {
            Database::transaction(function () use (&$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
                );
            });
        } catch (PDOException $exception) {
            if (($exception->errorInfo[1] ?? null) === 1062) {
                $this->failValidation(['cpf' => 'Já existe uma ficha cadastrada com este CPF.']);
            }

            throw $exception;
        }

        Session::clearOldInput();
        Session::clearErrors();
        Session::forget(self::PENDING_PROFILE_PHOTO_SESSION_KEY);
        Session::set(self::PENDING_DOCUMENTS_SESSION_KEY, (int) $admissionId);

        $admission = Admission::find((int) $admissionId);
        if ($admission === null) {
            Session::flash('error', 'A ficha foi salva, mas não foi possível abrir a etapa de documentos.');
            $this->redirectTo('');
        }

        Session::flash('success', 'Ficha enviada com sucesso. Agora envie os documentos em arquivos separados, um por vez.');
        redirect($this->documentsStepUrl($admission));
    }

    public function documents(): void
    {
        $admission = $this->resolveDocumentsAdmission($_GET);
        if ($admission === null) {
            Session::flash('error', 'Nenhuma ficha está aguardando envio de documentos.');
            $this->redirectTo('');
        }

        if (!$this->documentsStillPending($admission)) {
            Session::forget(self::PENDING_DOCUMENTS_SESSION_KEY);

            if ($this->profilePhotoStillPending($admission)) {
                Session::set(self::PENDING_PROFILE_PHOTO_SESSION_KEY, (int) $admission['id']);
                Session::flash('success', 'Os documentos obrigatórios já foram enviados. Agora falta só a foto de perfil.');
                redirect($this->profilePhotoStepUrl($admission));
            }

            Session::forget(self::PENDING_PROFILE_PHOTO_SESSION_KEY);
            Session::flash('success', 'Esta ficha já foi concluída.');
            $this->redirectTo('obrigado');
        }

        $config = require dirname(__DIR__, 2) . '/config/app.php';
        $dependents = Dependent::forAdmission((int) $admission['id']);
        $attachments = Attachment::forAdmission((int) $admission['id']);

        Session::set(self::PENDING_DOCUMENTS_SESSION_KEY, (int) $admission['id']);
        Session::forget(self::PENDING_PROFILE_PHOTO_SESSION_KEY);

        $this->view('admissions/documents', [
            'config' => $config,
            'admission' => $admission,
            'dependents' => $dependents,
            'documentAccessToken' => $this->makeStepToken('documents', $admission),
            'documentUploadItems' => AdmissionForm::documentUploadItemsForAdmission($admission, $dependents, $attachments),
            'errors' => Session::errors(),
            'error' => Session::getFlash('error'),
            'success' => Session::getFlash('success'),
            'formTimeoutSeconds' => RequestLimits::formTimeoutSeconds(),
            'maxUploadFileBytes' => RequestLimits::uploadMaxFileBytes(),
            'maxUploadFileLabel' => RequestLimits::uploadMaxFileLabel(),
            'postMaxBytes' => RequestLimits::postMaxBytes(),
            'postMaxLabel' => RequestLimits::postMaxLabel(),
        ]);
    }

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

        if ($this->isOversizedSubmission()) {
            Session::flash('error', 'O envio do documento falhou porque o arquivo passou do limite do PHP (' . RequestLimits::postMaxLabel() . '). Reduza o arquivo e tente novamente.');
            redirect($this->documentsStepUrl($admission));
        }

        if ($this->isExpiredSubmission()) {
            $timeoutMinutes = max(1, (int) ceil(RequestLimits::formTimeoutSeconds() / 60));
            Session::flash('error', 'O tempo máximo para enviar documentos é de ' . $timeoutMinutes . ' minuto(s). Reabra a etapa e tente novamente dentro desse prazo.');
            redirect($this->documentsStepUrl($admission));
        }

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

        $labelKey = trim((string) ($_POST['label_key'] ?? ''));
        $file = $_FILES['attachment_file'] ?? null;
        $errors = [];
        $allowedLabelKeys = $this->requiredDocumentLabelsForAdmission($admission);

        if ($labelKey === '') {
            $errors['documents'] = 'Não foi possível identificar qual documento você tentou enviar.';
        } elseif (!in_array($labelKey, $allowedLabelKeys, true)) {
            $errors['documents'] = 'Este documento não está disponível para esta ficha.';
        }

        if (!is_array($file) || (int) ($file['error'] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_NO_FILE) {
            $errors[$labelKey !== '' ? $labelKey : 'documents'] = 'Selecione o arquivo antes de enviar este documento.';
        } else {
            try {
                $fileError = AdmissionAttachments::validateSingle($file, $labelKey);
                if ($fileError !== null) {
                    $errors[$labelKey] = $fileError;
                }
            } catch (\InvalidArgumentException $exception) {
                $errors[$labelKey !== '' ? $labelKey : 'documents'] = 'Documento inválido.';
            }
        }

        if ($errors !== []) {
            Session::flash('error', 'Revise o documento selecionado e tente novamente.');
            Session::putErrors($errors);
            redirect($this->documentsStepUrl($admission));
        }

        $storedPath = null;
        $replacedPath = null;

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

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

        Session::clearOldInput();
        Session::clearErrors();
        Session::flash('success', 'Documento enviado com sucesso.');
        redirect($this->documentsStepUrl($admission));
    }

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

        if ($this->isExpiredSubmission()) {
            $timeoutMinutes = max(1, (int) ceil(RequestLimits::formTimeoutSeconds() / 60));
            Session::flash('error', 'O tempo máximo para concluir os documentos é de ' . $timeoutMinutes . ' minuto(s). Reabra a etapa e tente novamente.');
            redirect($this->documentsStepUrl($admission));
        }

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

        $missingErrors = $this->missingRequiredDocuments($admission);
        if ($missingErrors !== []) {
            Session::flash('error', 'Ainda faltam documentos obrigatórios. Envie o que está pendente para continuar.');
            Session::putErrors($missingErrors);
            redirect($this->documentsStepUrl($admission));
        }

        Session::forget(self::PENDING_DOCUMENTS_SESSION_KEY);
        Session::set(self::PENDING_PROFILE_PHOTO_SESSION_KEY, (int) $admission['id']);
        Session::clearOldInput();
        Session::clearErrors();
        Session::flash('success', 'Documentos enviados. Agora falta só a foto de perfil.');
        redirect($this->profilePhotoStepUrl($admission));
    }

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

        if (!$this->profilePhotoStillPending($admission)) {
            Session::forget(self::PENDING_PROFILE_PHOTO_SESSION_KEY);
            Session::forget(self::PENDING_DOCUMENTS_SESSION_KEY);
            Session::flash('success', 'A foto de perfil desta ficha já foi enviada.');
            $this->redirectTo('obrigado');
        }

        if ($this->documentsStillPending($admission)) {
            Session::forget(self::PENDING_PROFILE_PHOTO_SESSION_KEY);
            Session::set(self::PENDING_DOCUMENTS_SESSION_KEY, (int) $admission['id']);
            Session::flash('error', 'Antes da foto de perfil, finalize o envio dos documentos obrigatórios.');
            redirect($this->documentsStepUrl($admission));
        }

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

        Session::set(self::PENDING_PROFILE_PHOTO_SESSION_KEY, (int) $admission['id']);
        Session::forget(self::PENDING_DOCUMENTS_SESSION_KEY);

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

    public function storeProfilePhoto(): void
    {
        $admission = $this->resolveProfilePhotoAdmission($_POST);
        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->documentsStillPending($admission)) {
            Session::forget(self::PENDING_PROFILE_PHOTO_SESSION_KEY);
            Session::set(self::PENDING_DOCUMENTS_SESSION_KEY, (int) $admission['id']);
            Session::flash('error', 'Finalize os documentos obrigatórios antes de enviar a foto de perfil.');
            redirect($this->documentsStepUrl($admission));
        }

        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.');
            redirect($this->profilePhotoStepUrl($admission));
        }

        if ($this->isExpiredSubmission()) {
            $timeoutMinutes = max(1, (int) ceil(RequestLimits::formTimeoutSeconds() / 60));
            Session::flash('error', 'O tempo máximo para enviar a foto de perfil é de ' . $timeoutMinutes . ' minuto(s). Reabra a etapa e tente novamente dentro desse prazo.');
            redirect($this->profilePhotoStepUrl($admission));
        }

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

        $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, 'profile_photo');
            if ($fileError !== null) {
                $errors['profile_photo'] = $fileError;
            }
        }

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

        $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()]);
            redirect($this->profilePhotoStepUrl($admission));
        } catch (Throwable $exception) {
            $this->cleanupStoredPaths(array_filter([$storedPath]));
            throw $exception;
        }

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

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

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

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

    private function resolveDocumentsAdmission(array $source = []): ?array
    {
        return $this->resolveStepAdmission($source, self::PENDING_DOCUMENTS_SESSION_KEY, 'documents', 'document_access_token');
    }

    private function resolveProfilePhotoAdmission(array $source = []): ?array
    {
        return $this->resolveStepAdmission($source, self::PENDING_PROFILE_PHOTO_SESSION_KEY, 'profile-photo', 'photo_access_token');
    }

    private function pendingStepUrl(): ?string
    {
        $documentsAdmission = $this->pendingAdmissionFromSession(self::PENDING_DOCUMENTS_SESSION_KEY);
        if ($documentsAdmission !== null) {
            if ($this->documentsStillPending($documentsAdmission)) {
                Session::forget(self::PENDING_PROFILE_PHOTO_SESSION_KEY);
                Session::set(self::PENDING_DOCUMENTS_SESSION_KEY, (int) $documentsAdmission['id']);
                return $this->documentsStepUrl($documentsAdmission);
            }

            if ($this->profilePhotoStillPending($documentsAdmission)) {
                Session::forget(self::PENDING_DOCUMENTS_SESSION_KEY);
                Session::set(self::PENDING_PROFILE_PHOTO_SESSION_KEY, (int) $documentsAdmission['id']);
                return $this->profilePhotoStepUrl($documentsAdmission);
            }
        }

        $profilePhotoAdmission = $this->pendingAdmissionFromSession(self::PENDING_PROFILE_PHOTO_SESSION_KEY);
        if ($profilePhotoAdmission !== null) {
            if ($this->documentsStillPending($profilePhotoAdmission)) {
                Session::forget(self::PENDING_PROFILE_PHOTO_SESSION_KEY);
                Session::set(self::PENDING_DOCUMENTS_SESSION_KEY, (int) $profilePhotoAdmission['id']);
                return $this->documentsStepUrl($profilePhotoAdmission);
            }

            if ($this->profilePhotoStillPending($profilePhotoAdmission)) {
                Session::forget(self::PENDING_DOCUMENTS_SESSION_KEY);
                Session::set(self::PENDING_PROFILE_PHOTO_SESSION_KEY, (int) $profilePhotoAdmission['id']);
                return $this->profilePhotoStepUrl($profilePhotoAdmission);
            }
        }

        Session::forget(self::PENDING_DOCUMENTS_SESSION_KEY);
        Session::forget(self::PENDING_PROFILE_PHOTO_SESSION_KEY);

        return null;
    }

    private function pendingAdmissionFromSession(string $sessionKey): ?array
    {
        $admissionId = (int) Session::get($sessionKey, 0);
        if ($admissionId <= 0) {
            return null;
        }

        $admission = Admission::find($admissionId);
        if ($admission === null) {
            Session::forget($sessionKey);
            return null;
        }

        return $admission;
    }

    private function resolveStepAdmission(array $source, string $sessionKey, string $purpose, string $tokenField): ?array
    {
        $requestedAdmissionId = (int) ($source['admission_id'] ?? $source['id'] ?? 0);
        $requestedToken = trim((string) ($source[$tokenField] ?? $source['token'] ?? ''));

        if ($requestedAdmissionId > 0 || $requestedToken !== '') {
            if ($requestedAdmissionId <= 0 || $requestedToken === '') {
                return null;
            }

            $requestedAdmission = Admission::find($requestedAdmissionId);
            if ($requestedAdmission === null) {
                return null;
            }

            if (!$this->verifyStepToken($purpose, $requestedAdmission, $requestedToken)) {
                return null;
            }

            Session::set($sessionKey, (int) $requestedAdmission['id']);
            return $requestedAdmission;
        }

        $sessionAdmissionId = (int) Session::get($sessionKey, 0);
        if ($sessionAdmissionId <= 0) {
            return null;
        }

        $sessionAdmission = Admission::find($sessionAdmissionId);
        if ($sessionAdmission === null) {
            Session::forget($sessionKey);
            return null;
        }

        return $sessionAdmission;
    }

    private function documentsStepUrl(array $admission): string
    {
        $query = http_build_query([
            'id' => (int) $admission['id'],
            'token' => $this->makeStepToken('documents', $admission),
        ]);

        return baseUrl('admissoes/documentos') . '?' . $query;
    }

    private function profilePhotoStepUrl(array $admission): string
    {
        $query = http_build_query([
            'id' => (int) $admission['id'],
            'token' => $this->makeStepToken('profile-photo', $admission),
        ]);

        return baseUrl('admissoes/foto-perfil') . '?' . $query;
    }

    private function makeStepToken(string $purpose, array $admission): string
    {
        $payload = implode('|', [
            $purpose,
            (string) ($admission['id'] ?? ''),
            (string) ($admission['created_at'] ?? ''),
            (string) ($admission['cpf'] ?? ''),
        ]);

        return hash_hmac('sha256', $payload, $this->stepSecret());
    }

    private function verifyStepToken(string $purpose, array $admission, string $token): bool
    {
        $expectedToken = $this->makeStepToken($purpose, $admission);
        return hash_equals($expectedToken, trim($token));
    }

    private function stepSecret(): string
    {
        $configuredSecret = trim((string) env('APP_KEY', ''));
        if ($configuredSecret !== '') {
            return $configuredSecret;
        }

        return hash('sha256', implode('|', [
            'admission-step-secret',
            (string) env('APP_NAME', 'ficha-admissao'),
            (string) env('APP_URL', ''),
        ]));
    }

    /**
     * @return array<string, string>
     */
    private function requiredDocumentLabelsForAdmission(array $admission): array
    {
        $dependents = Dependent::forAdmission((int) $admission['id']);
        return AdmissionForm::requiredDocumentLabelsForAdmission($admission, $dependents);
    }

    private function documentsStillPending(array $admission): bool
    {
        return $this->missingRequiredDocuments($admission) !== [];
    }

    private function profilePhotoStillPending(array $admission): bool
    {
        return Attachment::findByAdmissionAndLabel((int) $admission['id'], 'profile_photo') === null;
    }

    private function missingRequiredDocuments(array $admission): array
    {
        $errors = [];

        foreach ($this->requiredDocumentLabelsForAdmission($admission) as $labelKey) {
            if (Attachment::findByAdmissionAndLabel((int) $admission['id'], $labelKey) !== null) {
                continue;
            }

            $errors[$labelKey] = 'Ainda falta enviar este documento.';
        }

        return $errors;
    }

    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();
    }
}
