<?php

declare(strict_types=1);

namespace App\Controller\Admin;

use App\Entity\Document;
use App\Entity\DocumentVersion;
use App\Entity\IngestJob;
use App\Entity\User;
use App\Service\DocumentService;
use App\Service\FormatText;
use App\Service\IngestJobService;
use App\Service\LockService;
use Doctrine\DBAL\Connection;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\HttpFoundation\File\Exception\FileException;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Uid\Uuid;

#[Route('/admin/documents')]
final class DocumentController extends AbstractController
{
    private const INGEST_DUPLICATE_WINDOW_SECONDS = 3;

    #[Route('', name: 'admin_documents', methods: ['GET'])]
    public function index(EntityManagerInterface $em): Response
    {
        $documents = $em->getRepository(Document::class)
            ->createQueryBuilder('d')
            ->leftJoin('d.versions', 'v')
            ->addSelect('v')
            ->leftJoin('d.currentVersion', 'cv')
            ->addSelect('cv')
            ->orderBy('d.createdAt', 'DESC')
            ->getQuery()
            ->getResult();

        return $this->render('admin/document/index.html.twig', [
            'documents' => $documents,
        ]);
    }

    #[Route(
        '/{id}',
        name: 'admin_document_show',
        requirements: ['id' => '[0-9a-fA-F\-]{36}'],
        methods: ['GET']
    )]
    public function show(string $id, EntityManagerInterface $em): Response
    {
        return $this->render('admin/document/show.html.twig', [
            'document' => $this->findDocument($id, $em),
        ]);
    }

    #[Route('/new', name: 'admin_document_new', methods: ['GET', 'POST'])]
    public function new(
        Request $request,
        DocumentService $documentService,
        FormatText $formatText,
        IngestJobService $jobService,
        ParameterBagInterface $params,
        EntityManagerInterface $em,
    ): Response {
        if (!$request->isMethod('POST')) {
            return $this->render('admin/document/new.html.twig');
        }

        if (!$this->isCsrfTokenValid('create_document', (string) $request->request->get('_token'))) {
            $this->addFlash('danger', 'Ungültiges CSRF-Token.');

            return $this->redirectToRoute('admin_document_new');
        }

        /** @var UploadedFile|null $file */
        $file = $request->files->get('file');
        if (!$file instanceof UploadedFile) {
            $this->addFlash('danger', 'Keine gültige Datei hochgeladen.');

            return $this->redirectToRoute('admin_document_new');
        }

        $title = $this->resolveDocumentTitle($request, $file, $formatText);
        if ($title === '') {
            $this->addFlash('danger', 'Titel ist erforderlich.');

            return $this->redirectToRoute('admin_document_new');
        }

        $user = $this->requireUser();
        $uploadDir = trim((string) $params->get('mto.vector.data.upload.path'));

        try {
            $this->ensureDir($uploadDir);
            $filePath = $this->moveUploadedFile($file, $uploadDir, $formatText);

            $document = $documentService->createDocument($title, $filePath, $user);
            $version = $document->getCurrentVersion();

            if (!$version instanceof DocumentVersion) {
                throw new \RuntimeException('Dokument erstellt, aber keine aktuelle Version vorhanden.');
            }

            $job = $jobService->startJob(
                IngestJob::TYPE_DOCUMENT_VERSION_ACTIVATE,
                $user,
                $version->getDocument()->getId(),
                $version->getId(),
                null,
                IngestJob::STATUS_QUEUED
            );

            $logFile = $this->prepareJobLogFile((string) $job->getId());
            $job->setLogPath($logFile);
            $em->flush();

            if (!$this->canExec()) {
                $jobService->markFailed($job, 'Server configuration does not allow background execution (exec disabled).');
                $this->addFlash('danger', 'Dokument erstellt, aber Ingest konnte nicht asynchron gestartet werden (exec deaktiviert).');

                return $this->redirectToRoute('admin_documents');
            }

            $this->startIngestJob((string) $job->getId(), $logFile);

            return $this->redirectToRoute('admin_job_show', [
                'id' => (string) $job->getId(),
            ]);
        } catch (\Throwable $e) {
            $this->addFlash('danger', $this->buildSafeErrorMessage($e, 'Dokument konnte nicht erstellt werden.'));

            return $this->redirectToRoute('admin_document_new');
        }
    }

    #[Route('/{id}/version/new', name: 'admin_document_version_new', requirements: ['id' => '[0-9a-fA-F\-]{36}'], methods: ['GET', 'POST'])]
    public function newVersion(
        string $id,
        Request $request,
        EntityManagerInterface $em,
        DocumentService $documentService,
        ParameterBagInterface $params,
        FormatText $formatText,
    ): Response {
        $document = $this->findDocument($id, $em);

        if (!$request->isMethod('POST')) {
            return $this->render('admin/document/new_version.html.twig', [
                'document' => $document,
            ]);
        }

        if (!$this->isCsrfTokenValid('create_document_version_' . $id, (string) $request->request->get('_token'))) {
            $this->addFlash('danger', 'Ungültiges CSRF-Token.');

            return $this->redirectToRoute('admin_document_version_new', ['id' => $id]);
        }

        /** @var UploadedFile|null $file */
        $file = $request->files->get('file');
        if (!$file instanceof UploadedFile) {
            $this->addFlash('danger', 'Datei ist erforderlich.');

            return $this->redirectToRoute('admin_document_version_new', ['id' => $id]);
        }

        try {
            $user = $this->requireUser();
            $uploadDir = trim((string) $params->get('mto.vector.data.upload.path'));
            $this->ensureDir($uploadDir);
            $filePath = $this->moveUploadedFile($file, $uploadDir, $formatText);

            $documentService->addVersion($document, $filePath, $user);
            $this->addFlash('success', 'Neue Dokumentversion wurde hochgeladen.');
        } catch (\Throwable $e) {
            $this->addFlash('danger', $this->buildSafeErrorMessage($e, 'Neue Dokumentversion konnte nicht erstellt werden.'));

            return $this->redirectToRoute('admin_document_version_new', ['id' => $id]);
        }

        return $this->redirectToRoute('admin_document_show', ['id' => $id]);
    }

    #[Route(
        '/version/{versionId}/activate',
        name: 'admin_document_version_activate',
        requirements: ['versionId' => '[0-9a-fA-F\-]{36}'],
        methods: ['POST']
    )]
    public function activateVersion(
        string $versionId,
        Request $request,
        EntityManagerInterface $em,
        DocumentService $documentService,
        IngestJobService $jobService,
    ): RedirectResponse {
        if (!$this->isCsrfTokenValid('activate_version_' . $versionId, (string) $request->request->get('_token'))) {
            throw $this->createAccessDeniedException();
        }

        $version = $this->findDocumentVersion($versionId, $em);

        try {
            $documentService->activateVersion($version);

            $job = $jobService->startJob(
                IngestJob::TYPE_DOCUMENT_VERSION_ACTIVATE,
                $this->requireUser(),
                $version->getDocument()->getId(),
                $version->getId(),
                null,
                IngestJob::STATUS_QUEUED
            );

            $logFile = $this->prepareJobLogFile((string) $job->getId());
            $job->setLogPath($logFile);
            $em->flush();

            if (!$this->canExec()) {
                $jobService->markFailed($job, 'Server configuration does not allow background execution (exec disabled).');
                $this->addFlash('danger', 'Aktivierung ok, aber Ingest konnte nicht asynchron gestartet werden (exec deaktiviert).');

                return $this->redirectToRoute('admin_document_show', [
                    'id' => (string) $version->getDocument()->getId(),
                ]);
            }

            $this->startIngestJob((string) $job->getId(), $logFile);
            $this->addFlash('success', 'Version aktiviert. Ingest-Job wurde erstellt und gestartet.');

            return $this->redirectToRoute('admin_job_show', [
                'id' => (string) $job->getId(),
            ]);
        } catch (\Throwable $e) {
            $this->addFlash('danger', 'Aktivierung/Re-Ingest fehlgeschlagen: ' . $this->buildSafeErrorMessage($e, 'Unbekannter Fehler.'));
        }

        return $this->redirectToRoute('admin_document_show', [
            'id' => (string) $version->getDocument()->getId(),
        ]);
    }

    #[Route(
        '/version/{versionId}/ingest',
        name: 'admin_document_version_ingest',
        requirements: ['versionId' => '[0-9a-fA-F\-]{36}'],
        methods: ['POST']
    )]
    public function ingestVersion(
        string $versionId,
        Request $request,
        EntityManagerInterface $em,
        IngestJobService $jobService,
    ): RedirectResponse {
        if (!$this->isCsrfTokenValid('ingest_version_' . $versionId, (string) $request->request->get('_token'))) {
            throw $this->createAccessDeniedException();
        }

        $version = $this->findDocumentVersion($versionId, $em);

        /** @var IngestJob|null $existing */
        $existing = $em->getRepository(IngestJob::class)
            ->findOneBy(
                ['documentVersionId' => $version->getId()],
                ['startedAt' => 'DESC', 'id' => 'DESC']
            );

        if (
            $existing instanceof IngestJob
            && $existing->getStartedAt() > new \DateTimeImmutable('-' . self::INGEST_DUPLICATE_WINDOW_SECONDS . ' seconds')
            && in_array($existing->getStatus(), [IngestJob::STATUS_QUEUED, IngestJob::STATUS_RUNNING], true)
        ) {
            $this->addFlash('info', 'Für diese Version läuft bereits ein aktueller Ingest-Job.');

            return $this->redirectToRoute('admin_job_show', [
                'id' => (string) $existing->getId(),
            ]);
        }

        $job = $jobService->startJob(
            IngestJob::TYPE_DOCUMENT,
            $this->requireUser(),
            $version->getDocument()->getId(),
            $version->getId(),
            null,
            IngestJob::STATUS_QUEUED
        );

        $logFile = $this->prepareJobLogFile((string) $job->getId());
        $job->setLogPath($logFile);
        $em->flush();

        if (!$this->canExec()) {
            $jobService->markFailed($job, 'Server configuration does not allow background execution (exec disabled).');
            $this->addFlash('danger', 'Ingest konnte nicht asynchron gestartet werden (exec deaktiviert).');

            return $this->redirectToRoute('admin_document_show', [
                'id' => (string) $version->getDocument()->getId(),
            ]);
        }

        try {
            $this->startIngestJob((string) $job->getId(), $logFile);
        } catch (\Throwable $e) {
            $jobService->markFailed($job, 'Ingest async start failed: ' . $e->getMessage());
            $this->addFlash('danger', $this->buildSafeErrorMessage($e, 'Ingest konnte nicht gestartet werden.'));

            return $this->redirectToRoute('admin_document_show', [
                'id' => (string) $version->getDocument()->getId(),
            ]);
        }

        return $this->redirectToRoute('admin_job_show', [
            'id' => (string) $job->getId(),
        ]);
    }

    #[Route('/reset', name: 'admin_document_reset', methods: ['POST'])]
    public function resetCompleteSystem(
        Request $request,
        ParameterBagInterface $params,
        Connection $connection,
    ): RedirectResponse {
        $this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');

        if (!$this->isCsrfTokenValid('system_reset', (string) $request->request->get('_token'))) {
            $this->addFlash('danger', 'Ungültiges CSRF-Token.');

            return $this->redirectToRoute('admin_dashboard');
        }

        if (!$this->canExec()) {
            $this->addFlash('danger', 'Der Reset konnte nicht gestartet werden (exec deaktiviert).');

            return $this->redirectToRoute('admin_dashboard');
        }

        foreach ([
                     'mto.knowledge.ndjson',
                     'mto.knowledge.vector_index',
                     'mto.knowledge.vector_index_meta',
                     'mto.knowledge.index_meta',
                     'mto.runtime.meta',
                     'mto.knowledge.tags_ndjson',
                     'mto.knowledge.vector_tags_index',
                     'mto.knowledge.vector_tags_index_meta',
                 ] as $parameterName) {
            $path = trim((string) $params->get($parameterName));
            if ($path !== '' && is_file($path)) {
                @unlink($path);
            }
        }

        $uploadDir = trim((string) $params->get('mto.knowledge.upload'));
        if ($uploadDir !== '' && is_dir($uploadDir)) {
            exec('rm -rf ' . escapeshellarg($uploadDir));
        }

        $lockDir = trim((string) $params->get('mto.locks.dir'));
        if ($lockDir !== '' && is_dir($lockDir)) {
            exec('rm -rf ' . escapeshellarg($lockDir));
        }

        $sql = <<<'SQL'
SET FOREIGN_KEY_CHECKS = 0;
TRUNCATE TABLE db.document_tag;
TRUNCATE TABLE db.tag_rebuild_job;
TRUNCATE TABLE db.knowledge_tag;
TRUNCATE TABLE db.ingest_job;
TRUNCATE TABLE db.document_version;
TRUNCATE TABLE db.document;
SET FOREIGN_KEY_CHECKS = 1;
SQL;

        $connection->executeStatement($sql);

        $this->addFlash('success', 'Das System wurde erfolgreich zurückgesetzt.');

        return $this->redirectToRoute('admin_dashboard');
    }

    #[Route(
        '/{id}/delete',
        name: 'admin_document_delete',
        requirements: ['id' => '[0-9a-fA-F\-]{36}'],
        methods: ['POST']
    )]
    public function deleteDocument(
        string $id,
        Request $request,
        EntityManagerInterface $em,
        IngestJobService $jobService,
        LockService $lockService,
    ): RedirectResponse {
        $this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN');

        if (!$this->isCsrfTokenValid('delete_document_' . $id, (string) $request->request->get('_token'))) {
            throw $this->createAccessDeniedException();
        }

        $document = $this->findDocument($id, $em);

        if (!$lockService->acquire()) {
            $this->addFlash('danger', 'Ein Ingest-Job läuft bereits. Löschen derzeit nicht möglich.');

            return $this->redirectToRoute('admin_documents');
        }
        $lockService->release();

        $job = $jobService->startJob(
            IngestJob::TYPE_DOCUMENT_DELETE,
            $this->requireUser(),
            $document->getId(),
            null,
            null,
            IngestJob::STATUS_QUEUED
        );

        $logFile = $this->prepareJobLogFile((string) $job->getId());
        $job->setLogPath($logFile);
        $em->flush();

        if (!$this->canExec()) {
            $jobService->markFailed($job, 'Server configuration does not allow background execution (exec disabled).');
            $this->addFlash('danger', 'Löschen konnte nicht gestartet werden (exec deaktiviert).');

            return $this->redirectToRoute('admin_documents');
        }

        try {
            $this->startIngestJob((string) $job->getId(), $logFile);
        } catch (\Throwable $e) {
            $jobService->markFailed($job, 'Delete async start failed: ' . $e->getMessage());
            $this->addFlash('danger', $this->buildSafeErrorMessage($e, 'Löschvorgang konnte nicht gestartet werden.'));

            return $this->redirectToRoute('admin_documents');
        }

        $this->addFlash('success', 'Löschvorgang gestartet. Dokument wird nach Index-Rebuild entfernt.');

        return $this->redirectToRoute('admin_job_show', [
            'id' => (string) $job->getId(),
        ]);
    }

    private function canExec(): bool
    {
        if (!function_exists('exec')) {
            return false;
        }

        $disabled = (string) ini_get('disable_functions');
        if ($disabled === '') {
            return true;
        }

        $list = array_map('trim', explode(',', $disabled));

        return !in_array('exec', $list, true);
    }

    private function ensureDir(string $dir): void
    {
        if ($dir === '') {
            throw new \RuntimeException('Upload directory not configured.');
        }

        if (!is_dir($dir) && !mkdir($dir, 0775, true) && !is_dir($dir)) {
            throw new \RuntimeException('Unable to create upload directory.');
        }
    }

    private function moveUploadedFile(UploadedFile $file, string $uploadDir, FormatText $formatText): string
    {
        $originalName = trim((string) $file->getClientOriginalName());
        $baseName = pathinfo($originalName !== '' ? $originalName : 'document', PATHINFO_FILENAME);
        $extension = strtolower((string) $file->getClientOriginalExtension());

        $safeBaseName = $formatText->slugify($baseName !== '' ? $baseName : 'document');
        if ($safeBaseName === '') {
            $safeBaseName = 'document';
        }

        $newFilename = uniqid('', true) . '_' . $safeBaseName;
        if ($extension !== '') {
            $newFilename .= '.' . $extension;
        }

        try {
            $file->move($uploadDir, $newFilename);
        } catch (FileException) {
            throw new \RuntimeException('File upload failed.');
        }

        return rtrim($uploadDir, '/') . '/' . $newFilename;
    }

    private function resolveDocumentTitle(Request $request, UploadedFile $file, FormatText $formatText): string
    {
        $rawTitle = trim((string) $request->request->get('title', ''));
        if ($rawTitle !== '') {
            return $rawTitle;
        }

        $originalName = trim((string) $file->getClientOriginalName());
        $baseName = pathinfo($originalName, PATHINFO_FILENAME);

        return trim((string) $formatText->slugify($baseName !== '' ? $baseName : $originalName));
    }

    private function startIngestJob(string $jobId, string $logFile): void
    {
        $projectDir = $this->resolveProjectDir();
        $console = $projectDir . '/bin/console';

        if (!is_file($console)) {
            throw new \RuntimeException('bin/console not found: ' . $console);
        }

        $php = $this->resolvePhpBinary();

        $cmd = sprintf(
            'cd %s && nohup %s %s %s %s --no-interaction >> %s 2>&1 & echo $!',
            escapeshellarg($projectDir),
            escapeshellarg($php),
            escapeshellarg($console),
            escapeshellarg('mto:agent:ingest:run'),
            escapeshellarg($jobId),
            escapeshellarg($logFile),
        );

        $output = [];
        $exitCode = 0;
        @exec($cmd, $output, $exitCode);

        if ($exitCode !== 0) {
            throw new \RuntimeException('Background ingest bootstrap failed with exit code ' . $exitCode . '.');
        }
    }

    private function prepareJobLogFile(string $jobId): string
    {
        $projectDir = $this->resolveProjectDir();
        $logDir = $projectDir . '/var/log/ingest';
        $this->ensureDir($logDir);

        return $logDir . '/job_' . $jobId . '.log';
    }

    private function resolveProjectDir(): string
    {
        $projectDir = trim((string) $this->getParameter('kernel.project_dir'));

        if ($projectDir === '' || !is_dir($projectDir)) {
            throw new \RuntimeException('Project directory is invalid.');
        }

        return rtrim($projectDir, '/');
    }

    private function resolvePhpBinary(): string
    {
        $envCandidates = [
            trim((string) ($_SERVER['PHP_CLI_BINARY'] ?? '')),
            trim((string) ($_ENV['PHP_CLI_BINARY'] ?? '')),
            trim((string) getenv('PHP_CLI_BINARY')),
        ];

        foreach ($envCandidates as $candidate) {
            if ($this->isValidCliPhpBinary($candidate)) {
                return $candidate;
            }
        }

        $phpBinary = defined('PHP_BINARY') ? trim((string) PHP_BINARY) : '';
        if ($this->isValidCliPhpBinary($phpBinary)) {
            return $phpBinary;
        }

        $fallbackCandidates = [
            '/usr/bin/php',
            '/usr/local/bin/php',
            '/bin/php',
            '/opt/homebrew/bin/php',
        ];

        foreach ($fallbackCandidates as $candidate) {
            if ($this->isValidCliPhpBinary($candidate)) {
                return $candidate;
            }
        }

        $whichPhp = trim((string) @shell_exec('command -v php 2>/dev/null'));
        if ($this->isValidCliPhpBinary($whichPhp)) {
            return $whichPhp;
        }

        throw new \RuntimeException(
            'Could not resolve a CLI PHP binary. Set PHP_CLI_BINARY explicitly, e.g. /usr/bin/php.'
        );
    }

    private function isValidCliPhpBinary(string $path): bool
    {
        $path = trim($path);

        if ($path === '' || !is_file($path) || !is_executable($path)) {
            return false;
        }

        $basename = strtolower(basename($path));

        if (str_contains($basename, 'fpm') || str_contains($basename, 'cgi')) {
            return false;
        }

        return true;
    }

    private function findDocument(string $id, EntityManagerInterface $em): Document
    {
        try {
            $uuid = Uuid::fromString(trim($id));
        } catch (\Throwable) {
            throw new NotFoundHttpException();
        }

        /** @var Document|null $document */
        $document = $em->getRepository(Document::class)->find($uuid);

        if (!$document instanceof Document) {
            throw new NotFoundHttpException();
        }

        return $document;
    }

    private function findDocumentVersion(string $versionId, EntityManagerInterface $em): DocumentVersion
    {
        try {
            $uuid = Uuid::fromString(trim($versionId));
        } catch (\Throwable) {
            throw new NotFoundHttpException();
        }

        /** @var DocumentVersion|null $version */
        $version = $em->getRepository(DocumentVersion::class)->find($uuid);

        if (!$version instanceof DocumentVersion) {
            throw new NotFoundHttpException();
        }

        return $version;
    }

    private function requireUser(): User
    {
        $user = $this->getUser();

        if (!$user instanceof User) {
            throw new \RuntimeException('No authenticated user available.');
        }

        return $user;
    }

    private function buildSafeErrorMessage(\Throwable $e, string $fallback): string
    {
        $message = trim($e->getMessage());

        return $message !== '' ? $message : $fallback;
    }
}