<?php

namespace App\Services;

use App\Core\helpers\HTTP;
use App\Core\Middlewares\Authentication;
use App\Repositories\ProjectRepository;
use App\Models\Project;
use App\Repositories\StageRepository;
use Exception;
use ZipArchive;

class ProjectService {

    use HTTP;
    
    // Servicios
    private StageService $stageService;
    // Repositorios
    private ProjectRepository $projectRepository;
    private StageRepository $stageRepository;

    public function __construct(){
        $this->projectRepository = new ProjectRepository();
        $this->stageRepository = new StageRepository();
        $this->stageService = new StageService();
    }

    public function getProjects(): array 
    {
        // Obtenemos el id del usuario en sesión
        $user = Authentication::user();
        $allProjects = []; // Inicializamos 
        
        if($user->role->name === 'Administrador'){
            $allProjects = $this->projectRepository->getAll(); // Obtenemos todos los proyectos siempre y cuando seamos administradores
        }
        
        $myProjects = $this->projectRepository->getMyProjects($user->id); // Obtenemos los proyectos a los que estamos asociados

        return [
            'projects' => $allProjects,
            'myProjects' => $myProjects
        ];
    }

    public function getProjectById(int $id): ?Project 
    {   
        $project = $this->projectRepository->FindBy('id',$id);

        if(!$project){
            throw new Exception("No se encontraron registros de ese proyecto.");
        }

        $project->stages = $this->stageRepository->getStageTreeByProjectId($project->id);
        return $project;
    }

    public function createProject(Project $project, bool $isConfigured) : ?Project
    {
        $user = Authentication::user();
        $project->user_created = $user->id;
        
        $projectCreatedId = $this->projectRepository->create($project);

        if(!$projectCreatedId){
            throw new Exception("Hubo un error al crear el proyecto en la bd.", 500);
        }

        $this->createProjectFolder($projectCreatedId, $project->name);

        if($isConfigured){
            $structureJson = json_decode(file_get_contents(__DIR__ . "/../../ProjectDefaultStructure.json"), true);
            $this->stageService->createStageTree($projectCreatedId, $structureJson);
        }

        return $this->getProjectById($projectCreatedId);
    }

    private function createProjectFolder(int $projectId, string $projectName): void
    {
        $sanitizedName = $this->sanitizeFolderName($projectName);
        $projectPath = __DIR__ . '/../../Storage/projects/' . $sanitizedName;
        
        if (!is_dir($projectPath)) {
            if(!mkdir($projectPath, 0777, true)){
                throw new Exception("No se pudo crear la carpeta del proyecto.", 500);
            }
        }
    }

    private function sanitizeFolderName(string $name): string
    {
        $name = preg_replace('/[<>:"\/\\\\|?*]/', '_', $name);

        $name = trim($name);

        $name = preg_replace('/\s+/', ' ', $name);
        
        return $name;
    }

    public function getProjectUsers(int $projectId): ?array
    {
        $usersInProject = $this->projectRepository->getUsersIn($projectId);
        $usersNotInProject = $this->projectRepository->getUsersNotIn($projectId);

        return [
            'users' => $usersInProject, 
            'restUsers' => $usersNotInProject
        ];
    }

    public function addUserProject($userId, $projectId)
    {
        if(!$this->projectRepository->addUserIn($userId, $projectId)){
            throw new Exception("Hubo un error al asignar el usuario al proyecto.", 500);
        }

        $usersInProject = $this->projectRepository->getUsersIn($projectId);
        $usersNotInProject = $this->projectRepository->getUsersNotIn($projectId);

        return [
            $usersInProject, 
            $usersNotInProject
        ];
    }

    public function removeUserFromProject(int $projectId, int $userId): array
    {

        $isDeleted = $this->projectRepository->removeUser($projectId, $userId);

        if(!$isDeleted){
            throw new Exception("Hubo un error al remover el usuario del proyecto", 400);
        }

        $usersInProject = $this->projectRepository->getUsersIn($projectId);
        $usersNotInProject = $this->projectRepository->getUsersNotIn($projectId);

        return [
            $usersInProject, 
            $usersNotInProject
        ];
    }

    public function validateHasProject(int $projectId)
    {
        $user = Authentication::user();

        if($user->role->name === 'Administrador') return; 
        
        if(!$this->projectRepository->userHas($projectId, $user->id)){
            throw new Exception("No tienes acceso a ver este proyecto", 403);
        }
        return;
    }

    public function deleteProject(int $projectId): bool
    {
        $project = $this->projectRepository->FindBy('id', $projectId);
        
        if(!$project){
            throw new Exception("Proyecto no encontrado.", 404);
        }

        $this->deleteProjectFolder($project->name);

        return true;
    }

    private function deleteProjectFolder(string $projectName): void
    {
        $sanitizedName = $this->sanitizeFolderName($projectName);
        $projectPath = __DIR__ . '/../../Storage/projects/' . $sanitizedName;
        
        if (is_dir($projectPath)) {
            $this->deleteDirectory($projectPath);
        }
    }

    private function deleteDirectory(string $dir): bool
    {
        if (!file_exists($dir)) {
            return true;
        }

        if (!is_dir($dir)) {
            return unlink($dir);
        }

        foreach (scandir($dir) as $item) {
            if ($item == '.' || $item == '..') {
                continue;
            }

            if (!$this->deleteDirectory($dir . DIRECTORY_SEPARATOR . $item)) {
                return false;
            }
        }

        return rmdir($dir);
    }

    /**
     * Descarga todo el proyecto como archivo ZIP
     */
    public function downloadProject(int $projectId): void
    {
        $project = $this->projectRepository->FindBy('id', $projectId);
        
        if (!$project) {
            throw new Exception("Proyecto no encontrado.", 404);
        }

        $sanitizedName = $this->sanitizeFolderName($project->name);
        $projectPath = __DIR__ . '/../../Storage/projects/' . $sanitizedName;
        
        if (!is_dir($projectPath)) {
            throw new Exception("La carpeta del proyecto no existe.", 404);
        }

        // Verificar si hay archivos en el proyecto
        $isEmpty = $this->isDirectoryEmpty($projectPath);

        // Crear archivo ZIP temporal
        $zipFileName = $sanitizedName . '_' . date('Y-m-d_H-i-s') . '.zip';
        $zipPath = sys_get_temp_dir() . DIRECTORY_SEPARATOR . $zipFileName;

        $zip = new ZipArchive();
        if ($zip->open($zipPath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
            throw new Exception("No se pudo crear el archivo ZIP.", 500);
        }

        if ($isEmpty) {
            // Si está vacío, crear la estructura de carpetas de las etapas
            $stages = $this->stageRepository->getStageTreeByProjectId($projectId);
            $this->addStageStructureToZip($zip, $stages, $sanitizedName);
            
            // Agregar un archivo README explicativo en la raíz
            $readmeContent = "Este proyecto aún no tiene documentos cargados.\n\n";
            $readmeContent .= "Proyecto: " . $project->name . "\n";
            $readmeContent .= "Fecha de descarga: " . date('Y-m-d H:i:s') . "\n\n";
            $readmeContent .= "La estructura de carpetas representa las etapas del proyecto.\n";
            $readmeContent .= "Los documentos se agregarán en sus respectivas carpetas cuando sean subidos.";
            
            $zip->addFromString($sanitizedName . '/README.txt', $readmeContent);
        } else {
            // Agregar archivos al ZIP recursivamente
            $this->addFilesToZip($zip, $projectPath, $sanitizedName);
        }
        
        $zip->close();

        // Enviar el archivo al navegador
        header('Content-Type: application/zip');
        header('Content-Disposition: attachment; filename="' . $zipFileName . '"');
        header('Content-Length: ' . filesize($zipPath));
        header('Cache-Control: no-cache, must-revalidate');
        header('Pragma: public');

        // Limpiar el buffer de salida
        if (ob_get_level()) {
            ob_clean();
        }
        flush();

        readfile($zipPath);

        // Eliminar archivo temporal
        unlink($zipPath);
        exit;
    }

    /**
     * Agrega archivos recursivamente al ZIP
     */
    private function addFilesToZip(ZipArchive $zip, string $dir, string $zipPath): void
    {
        $files = new \RecursiveIteratorIterator(
            new \RecursiveDirectoryIterator($dir),
            \RecursiveIteratorIterator::LEAVES_ONLY
        );

        foreach ($files as $file) {
            if (!$file->isDir()) {
                $filePath = $file->getRealPath();
                $relativePath = $zipPath . DIRECTORY_SEPARATOR . substr($filePath, strlen($dir) + 1);
                
                // Normalizar separadores de directorio para ZIP
                $relativePath = str_replace('\\', '/', $relativePath);
                
                $zip->addFile($filePath, $relativePath);
            }
        }
    }

    /**
     * Verifica si un directorio está vacío (no tiene archivos, solo subdirectorios vacíos)
     */
    private function isDirectoryEmpty(string $dir): bool
    {
        $iterator = new \RecursiveIteratorIterator(
            new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS),
            \RecursiveIteratorIterator::LEAVES_ONLY
        );

        foreach ($iterator as $file) {
            if ($file->isFile()) {
                return false;
            }
        }

        return true;
    }

    /**
     * Agrega la estructura de etapas al ZIP (carpetas vacías)
     */
    private function addStageStructureToZip(ZipArchive $zip, array $stages, string $basePath = ''): void
    {
        foreach ($stages as $stage) {
            $stagePath = $basePath . '/' . $this->sanitizeFolderName($stage->name);
            
            // Agregar carpeta vacía al ZIP (usando un archivo oculto)
            $zip->addFromString($stagePath . '/.gitkeep', '');
            
            // Si tiene sub-etapas, procesarlas recursivamente
            if (!empty($stage->children)) {
                $this->addStageStructureToZip($zip, $stage->children, $stagePath);
            }
        }
    }
}