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

        $documentAttachmentFields = AdmissionForm::documentAttachmentFields();

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

        $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, $documentAttachmentFields): 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, $documentAttachmentFields);
                $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);

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

        Session::flash('success', 'Ficha enviada com sucesso. Falta só a foto de perfil, que agora é enviada separadamente para não pesar o formulário.');
        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('');
        }

        $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']);

        $this->view('admissions/profile-photo', [
            'config' => $config,
            'admission' => $admission,
            'profilePhotoAccessToken' => $this->makeProfilePhotoToken($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->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->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);
            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::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 resolveProfilePhotoAdmission(array $source = []): ?array
    {
        $requestedAdmissionId = (int) ($source['admission_id'] ?? $source['id'] ?? 0);
        $requestedToken = trim((string) ($source['photo_access_token'] ?? $source['token'] ?? ''));

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

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

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

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

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

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

        return $sessionAdmission;
    }

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

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

    private function makeProfilePhotoToken(array $admission): string
    {
        $payload = implode('|', [
            'profile-photo',
            (string) ($admission['id'] ?? ''),
            (string) ($admission['created_at'] ?? ''),
            (string) ($admission['cpf'] ?? ''),
        ]);

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

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

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

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

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