// IWYU pragma: no_include <bits/chrono.h>
#include "core/system/director.hpp"

#include <SDL3/SDL.h>  // Para SDL_SetLogPriority, SDL_LogCategory, SDL_LogPriority, SDL_Quit

#include <cstdlib>    // Para srand, exit, rand, EXIT_FAILURE
#include <ctime>      // Para time
#include <fstream>    // Para ifstream, ofstream
#include <iostream>   // Para basic_ostream, operator<<, cerr
#include <memory>     // Para make_unique, unique_ptr
#include <stdexcept>  // Para runtime_error
#include <string>     // Para allocator, basic_string, char_traits, operator+, string, operator==

#include "core/audio/audio.hpp"                    // Para Audio
#include "core/input/input.hpp"                    // Para Input
#include "core/locale/lang.hpp"                    // Para setLanguage
#include "core/rendering/screen.hpp"               // Para Screen
#include "core/resources/asset.hpp"                // Para Asset
#include "core/resources/resource.hpp"             // Para Resource
#include "core/resources/resource_helper.hpp"      // Para initializeResourceSystem
#include "core/system/global_events.hpp"           // Para GlobalEvents::handle
#include "core/system/section.hpp"                 // Para Name, Options, name, options, AttractMode, attract_mode
#include "core/system/shutdown.hpp"                // Para resultToString, shutdownSystem, ShutdownResult
#include "core/system/system_utils.hpp"            // Para createApplicationFolder, resultToString, Result
#include "external/fkyaml_node.hpp"                // Para fkyaml::node
#include "game/entities/player.hpp"                // Para Player
#include "game/gameplay/manage_hiscore_table.hpp"  // Para ManageHiScoreTable
#include "game/options.hpp"                        // Para Settings, loadFromFile, saveToFile, settings, setConfigFile, setControllersFile
#include "game/scenes/credits.hpp"                 // Para Credits
#include "game/scenes/game.hpp"                    // Para Game
#include "game/scenes/hiscore_table.hpp"           // Para HiScoreTable
#include "game/scenes/instructions.hpp"            // Para Instructions
#include "game/scenes/intro.hpp"                   // Para Intro
#include "game/scenes/logo.hpp"                    // Para Logo
#include "game/scenes/preload.hpp"                 // Para Preload
#include "game/scenes/title.hpp"                   // Para Title
#include "game/ui/notifier.hpp"                    // Para Notifier
#include "game/ui/service_menu.hpp"                // Para ServiceMenu
#include "utils/param.hpp"                         // Para loadParamsFromFile

// Constructor
Director::Director() {
    Section::attract_mode = Section::AttractMode::TITLE_TO_DEMO;

    // Establece el nivel de prioridad de la categoría de registro
    SDL_SetLogPriority(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_INFO);
    SDL_SetLogPriority(SDL_LOG_CATEGORY_TEST, SDL_LOG_PRIORITY_ERROR);

    // Inicia la semilla aleatoria usando el tiempo actual en segundos
    std::srand(static_cast<unsigned int>(std::time(nullptr)));

    std::cout << "Game start\n";

    // Obtener la ruta del ejecutable desde SDL
#ifdef __EMSCRIPTEN__
    // En Emscripten los assets viven en la raíz del MEMFS virtual (/data, /config),
    // preloaded vía --preload-file en el linker. No hay ruta de ejecutable.
    executable_path_ = "";
#else
    const char* base_path = SDL_GetBasePath();
    executable_path_ = (base_path != nullptr) ? base_path : "";
#endif

    // Crea la carpeta del sistema donde guardar los datos persistentes
    createSystemFolder("jailgames");
    createSystemFolder("jailgames/coffee_crisis_arcade_edition");

    // Establecer sección inicial según modo de compilación
#ifdef RECORDING
    Section::name = Section::Name::GAME;
    Section::options = Section::Options::GAME_PLAY_1P;
#elif _DEBUG
    loadDebugConfig();
#else
    Section::name = Section::Name::LOGO;
    Section::options = Section::Options::NONE;
#endif

    init();
}

Director::~Director() {
    // Libera las secciones primero: sus destructores pueden tocar Audio/Resource/Screen,
    // que close() destruye a continuación.
    resetActiveSection();
    close();
}

// Inicializa todo
void Director::init() {
    // Configuración inicial de parametros
    Asset::init(executable_path_);  // Inicializa el sistema de gestión de archivos

    // Determinar ruta del pack según la plataforma
#ifdef MACOS_BUNDLE
    std::string pack_path = executable_path_ + "../Resources/resources.pack";
#else
    std::string pack_path = executable_path_ + "resources.pack";
#endif

    // Inicializar sistema de recursos con o sin fallback según el tipo de build
#if defined(RELEASE_BUILD) && !defined(__EMSCRIPTEN__)
    // Release nativo: Sin fallback - Solo resources.pack (estricto)
    ResourceHelper::initializeResourceSystem(pack_path, false);
#else
    // Desarrollo o Emscripten: Con fallback - carga desde filesystem/MEMFS
    ResourceHelper::initializeResourceSystem(pack_path, true);
#endif

    loadAssets();  // Crea el índice de archivos

    Input::init(Asset::get()->getPath("gamecontrollerdb.txt"), Asset::get()->getPath("controllers.json"));  // Carga configuración de controles

    Options::setConfigFile(Asset::get()->getPath("config.yaml"));            // Establece el fichero de configuración
    Options::setControllersFile(Asset::get()->getPath("controllers.json"));  // Establece el fichero de configuración de mandos
    Options::setPostFXFile(Asset::get()->getPath("postfx.yaml"));            // Establece el fichero de presets PostFX
    Options::setCrtPiFile(Asset::get()->getPath("crtpi.yaml"));              // Establece el fichero de presets CrtPi
    Options::loadFromFile();                                                 // Carga el archivo de configuración
    Options::loadPostFXFromFile();                                           // Carga los presets PostFX
    Options::loadCrtPiFromFile();                                            // Carga los presets CrtPi

#ifdef __EMSCRIPTEN__
    // En la versión web el navegador gestiona la ventana: ventana (no
    // fullscreen — el canvas ya marca el área), integer scale para píxeles nítidos.
    Options::window.zoom = 3;
    Options::video.fullscreen = false;
    Options::video.integer_scale = true;
    // Precarga silenciosa: pantalla negra mientras el .data termina de descargarse.
    Options::loading.show = false;
    Options::loading.wait_for_input = false;
#endif
    loadParams();     // Carga los parámetros del programa
    loadScoreFile();  // Carga el archivo de puntuaciones

    // Inicialización de subsistemas principales
    Lang::setLanguage(Options::settings.language);  // Carga el archivo de idioma

    Screen::init();  // Inicializa la pantalla y el sistema de renderizado

    Audio::init();  // Activa el sistema de audio

#ifdef _DEBUG
    Resource::init(debug_config.resource_loading == "lazy" ? Resource::LoadingMode::LAZY_LOAD : Resource::LoadingMode::PRELOAD);
#else
    Resource::init(Resource::LoadingMode::PRELOAD);
#endif

    if (Resource::get()->getLoadingMode() == Resource::LoadingMode::PRELOAD) {
        // Guarda la sección destino (la que fijó loadDebugConfig o el default)
        // y redirige el arranque a la escena PRELOAD hasta que loadStep termine.
        Section::post_preload = Section::name;
        Section::name = Section::Name::PRELOAD;
        Resource::get()->beginLoad();
    } else {
        // LAZY_LOAD: el constructor de Resource ya cargó lo esencial síncronamente;
        // no hay fase de preload, pasamos directamente a post-boot.
        finishBoot();
        boot_loading_ = false;
    }

    // ServiceMenu/Notifier/getSingletons se mueven a finishBoot() — dependen
    // de Resource y se inicializan al terminar la carga incremental.
}

// Inicializaciones que dependen del Resource cargado. Se llama desde iterate()
// cuando Resource::loadStep() devuelve true, con la ventana y el bucle vivos.
void Director::finishBoot() {
    ServiceMenu::init();                                                 // Inicializa el menú de servicio
    Notifier::init(std::string(), Resource::get()->getText("8bithud"));  // Inicialización del sistema de notificaciones
    Screen::get()->getSingletons();                                      // Obtiene los punteros al resto de singletones

    // Restaura el vsync a la preferencia del usuario (beginLoad lo había puesto a false)
    Screen::get()->setVSync(Options::video.vsync);

    // Si NO estamos en modo "wait for input", transiciona ya al destino.
    // Si wait_for_input está activo (y la pantalla es visible), nos quedamos
    // en PRELOAD hasta que el usuario pulse tecla/botón.
    if (!(Options::loading.show && Options::loading.wait_for_input)) {
        Section::name = Section::post_preload;
    }
}

// Cierra todo y libera recursos del sistema y de los singletons
void Director::close() {
    // Guarda las opciones actuales en el archivo de configuración
    Options::saveToFile();

    // Libera los singletons y recursos en orden inverso al de inicialización
    Notifier::destroy();     // Libera el sistema de notificaciones
    ServiceMenu::destroy();  // Libera el sistema de menú de servicio
    Input::destroy();        // Libera el sistema de entrada
    Resource::destroy();     // Libera el sistema de recursos gráficos y de texto
    Audio::destroy();        // Libera el sistema de audio
    Screen::destroy();       // Libera el sistema de pantalla y renderizado
    Asset::destroy();        // Libera el gestor de archivos

    std::cout << "\nBye!\n";

    // Libera todos los recursos de SDL
    SDL_Quit();

    // Apaga el sistema
    shutdownSystem(Section::options == Section::Options::SHUTDOWN);
}

// Carga los parametros
void Director::loadParams() {
    // Carga los parametros para configurar el juego
#ifdef ANBERNIC
    const std::string PARAM_FILE_PATH = Asset::get()->getPath("param_320x240.txt");
#else
    const std::string PARAM_FILE_PATH = Asset::get()->getPath(Options::settings.params_file);
#endif
    loadParamsFromFile(PARAM_FILE_PATH);
}

// Carga el fichero de puntuaciones
void Director::loadScoreFile() {
    auto manager = std::make_unique<ManageHiScoreTable>(Options::settings.hi_score_table);
#ifdef _DEBUG
    manager->clear();
#else
    manager->loadFromFile(Asset::get()->getPath("score.bin"));
#endif
}

// Carga el indice de ficheros desde un fichero
void Director::loadAssets() {
#ifdef MACOS_BUNDLE
    const std::string PREFIX = "/../Resources";
#else
    const std::string PREFIX;
#endif

    // Cargar la configuración de assets (también aplicar el prefijo al archivo de configuración)
    std::string config_path = executable_path_ + PREFIX + "/config/assets.txt";
    Asset::get()->loadFromFile(config_path, PREFIX, system_folder_);

    // Si falta algun fichero, sale del programa
    if (!Asset::get()->check()) {
        throw std::runtime_error("Falta algun fichero");
    }
}

// Carga debug.yaml desde la carpeta del sistema (solo en _DEBUG)
void Director::loadDebugConfig() {
    const std::string DEBUG_FILE = system_folder_ + "/debug.yaml";

    std::ifstream file(DEBUG_FILE);
    if (!file.good()) {
        // Crear fichero por defecto
        std::ofstream out(DEBUG_FILE);
        if (out.is_open()) {
            out << "# Coffee Crisis Arcade Edition - Debug Configuration\n";
            out << "# This file is only read in DEBUG builds.\n";
            out << "#\n";
            out << "# initial_section: logo, intro, title, game, credits, instructions, hiscore\n";
            out << "# initial_options: none, 1p, 2p, both\n";
            out << "# initial_stage: 0-based stage index (only when section is game)\n";
            out << "# show_render_info: show FPS/driver/preset overlay\n";
            out << "# resource_loading: preload, lazy\n";
            out << "\n";
            out << "initial_section: game\n";
            out << "initial_options: 1p\n";
            out << "initial_stage: 0\n";
            out << "show_render_info: true\n";
            out << "resource_loading: preload\n";
            out << "autoplay: false\n";
            out << "invincibility: false\n";
            out.close();
        }
        // Usar defaults de DebugConfig
    } else {
        std::string content((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
        file.close();
        try {
            auto yaml = fkyaml::node::deserialize(content);
            if (yaml.contains("initial_section")) {
                try {
                    debug_config.initial_section = yaml["initial_section"].get_value<std::string>();
                } catch (...) {}
            }
            if (yaml.contains("initial_options")) {
                try {
                    debug_config.initial_options = yaml["initial_options"].get_value<std::string>();
                } catch (...) {}
            }
            if (yaml.contains("initial_stage")) {
                try {
                    debug_config.initial_stage = yaml["initial_stage"].get_value<int>();
                } catch (...) {}
            }
            if (yaml.contains("show_render_info")) {
                try {
                    debug_config.show_render_info = yaml["show_render_info"].get_value<bool>();
                } catch (...) {}
            }
            if (yaml.contains("resource_loading")) {
                try {
                    debug_config.resource_loading = yaml["resource_loading"].get_value<std::string>();
                } catch (...) {}
            }
            if (yaml.contains("autoplay")) {
                try {
                    debug_config.autoplay = yaml["autoplay"].get_value<bool>();
                } catch (...) {}
            }
            if (yaml.contains("invincibility")) {
                try {
                    debug_config.invincibility = yaml["invincibility"].get_value<bool>();
                } catch (...) {}
            }
        } catch (...) {
            std::cout << "Error parsing debug.yaml, using defaults" << '\n';
        }
    }

    // Mapear strings a enums
    const auto& sec = debug_config.initial_section;
    if (sec == "logo") {
        Section::name = Section::Name::LOGO;
    } else if (sec == "intro") {
        Section::name = Section::Name::INTRO;
    } else if (sec == "title") {
        Section::name = Section::Name::TITLE;
    } else if (sec == "game") {
        Section::name = Section::Name::GAME;
    } else if (sec == "credits") {
        Section::name = Section::Name::CREDITS;
    } else if (sec == "instructions") {
        Section::name = Section::Name::INSTRUCTIONS;
    } else if (sec == "hiscore") {
        Section::name = Section::Name::HI_SCORE_TABLE;
    } else {
        Section::name = Section::Name::GAME;
    }

    const auto& opt = debug_config.initial_options;
    if (opt == "none") {
        Section::options = Section::Options::NONE;
    } else if (opt == "1p") {
        Section::options = Section::Options::GAME_PLAY_1P;
    } else if (opt == "2p") {
        Section::options = Section::Options::GAME_PLAY_2P;
    } else if (opt == "both") {
        Section::options = Section::Options::GAME_PLAY_BOTH;
    } else {
        Section::options = Section::Options::GAME_PLAY_1P;
    }
}

// Crea la carpeta del sistema donde guardar datos
void Director::createSystemFolder(const std::string& folder) {
    auto result = SystemUtils::createApplicationFolder(folder, system_folder_);

    if (result != SystemUtils::Result::SUCCESS) {
        std::cerr << "Error creando carpeta del sistema: "
                  << SystemUtils::resultToString(result) << '\n';
        exit(EXIT_FAILURE);
    }
}

// Libera todos los unique_ptr de sección (solo uno tiene propiedad a la vez)
void Director::resetActiveSection() {
    preload_.reset();
    logo_.reset();
    intro_.reset();
    title_.reset();
    game_.reset();
    instructions_.reset();
    hi_score_table_.reset();
    credits_.reset();
}

// Destruye la sección anterior y construye la nueva cuando Section::name cambia
void Director::handleSectionTransition() {
    // RESET: recarga recursos y vuelve a LOGO (el propio reset() cambia Section::name)
    if (Section::name == Section::Name::RESET) {
        resetActiveSection();  // libera recursos actuales antes del reload
        reset();
    }

    if (Section::name == last_built_section_name_) {
        return;  // ya tenemos la sección correcta viva
    }

    // Destruye la sección anterior
    resetActiveSection();

    // Construye la nueva
    switch (Section::name) {
        case Section::Name::PRELOAD:
            preload_ = std::make_unique<Preload>();
            break;

        case Section::Name::LOGO:
            logo_ = std::make_unique<Logo>();
            break;

        case Section::Name::INTRO:
            intro_ = std::make_unique<Intro>();
            break;

        case Section::Name::TITLE:
            title_ = std::make_unique<Title>();
            break;

        case Section::Name::GAME: {
            Player::Id player_id = Player::Id::PLAYER1;
            switch (Section::options) {
                case Section::Options::GAME_PLAY_1P:
                    player_id = Player::Id::PLAYER1;
                    break;
                case Section::Options::GAME_PLAY_2P:
                    player_id = Player::Id::PLAYER2;
                    break;
                case Section::Options::GAME_PLAY_BOTH:
                    player_id = Player::Id::BOTH_PLAYERS;
                    break;
                default:
                    break;
            }
#ifdef _DEBUG
            const int CURRENT_STAGE = debug_config.initial_stage;
#else
            constexpr int CURRENT_STAGE = 0;
#endif
            game_ = std::make_unique<Game>(player_id, CURRENT_STAGE, Game::DEMO_OFF);
            break;
        }

        case Section::Name::GAME_DEMO: {
            const auto PLAYER_ID = static_cast<Player::Id>((rand() % 2) + 1);
            constexpr auto CURRENT_STAGE = 0;
            game_ = std::make_unique<Game>(PLAYER_ID, CURRENT_STAGE, Game::DEMO_ON);
            break;
        }

        case Section::Name::INSTRUCTIONS:
            instructions_ = std::make_unique<Instructions>();
            break;

        case Section::Name::CREDITS:
            credits_ = std::make_unique<Credits>();
            break;

        case Section::Name::HI_SCORE_TABLE:
            hi_score_table_ = std::make_unique<HiScoreTable>();
            break;

        case Section::Name::RESET:
        case Section::Name::QUIT:
        default:
            break;
    }

    last_built_section_name_ = Section::name;
}

// Reinicia objetos y vuelve a la sección inicial
void Director::reset() {
    Options::saveToFile();
    Options::loadFromFile();
    Lang::setLanguage(Options::settings.language);
    Audio::get()->stopMusic();
    Audio::get()->stopAllSounds();
    Resource::get()->reload();
    ServiceMenu::get()->reset();
    Section::name = Section::Name::LOGO;
}

// Avanza un frame de la sección activa (llamado desde SDL_AppIterate)
auto Director::iterate() -> SDL_AppResult {
    if (Section::name == Section::Name::QUIT) {
        return SDL_APP_SUCCESS;
    }

    // Fase de boot: carga incremental frame a frame con presupuesto de 50ms.
    // Durante esta fase la escena activa es Preload (una barra de progreso).
    if (boot_loading_) {
        try {
            if (Resource::get()->loadStep(50 /*ms*/)) {
                finishBoot();
                boot_loading_ = false;
                // Los SDL_EVENT_GAMEPAD_ADDED iniciales ya los ha drenado la rama
                // durante la carga: marcamos startup completo ahora para que los
                // ADDED/REMOVED posteriores sí generen notificación.
                GlobalEvents::markStartupComplete();
            }
        } catch (const std::exception& e) {
            std::cerr << "Fatal error during resource load: " << e.what() << '\n';
            Section::name = Section::Name::QUIT;
            return SDL_APP_FAILURE;
        }
    }

    // Gestiona las transiciones entre secciones (destruye la anterior y construye la nueva)
    handleSectionTransition();

    // Ejecuta un frame de la sección activa
    if (preload_) {
        preload_->iterate();
    } else if (logo_) {
        logo_->iterate();
    } else if (intro_) {
        intro_->iterate();
    } else if (title_) {
        title_->iterate();
    } else if (game_) {
        game_->iterate();
    } else if (instructions_) {
        instructions_->iterate();
    } else if (hi_score_table_) {
        hi_score_table_->iterate();
    } else if (credits_) {
        credits_->iterate();
    }

    return (Section::name == Section::Name::QUIT) ? SDL_APP_SUCCESS : SDL_APP_CONTINUE;
}

// Procesa un evento SDL (llamado desde SDL_AppEvent)
auto Director::handleEvent(SDL_Event& event) -> SDL_AppResult {
    // Eventos globales (SDL_EVENT_QUIT, resize, render target reset, hotplug, service menu, ratón)
    GlobalEvents::handle(event);

    // Reenvía a la sección activa
    if (preload_) {
        preload_->handleEvent(event);
    } else if (logo_) {
        logo_->handleEvent(event);
    } else if (intro_) {
        intro_->handleEvent(event);
    } else if (title_) {
        title_->handleEvent(event);
    } else if (game_) {
        game_->handleEvent(event);
    } else if (instructions_) {
        instructions_->handleEvent(event);
    } else if (hi_score_table_) {
        hi_score_table_->handleEvent(event);
    } else if (credits_) {
        credits_->handleEvent(event);
    }

    return (Section::name == Section::Name::QUIT) ? SDL_APP_SUCCESS : SDL_APP_CONTINUE;
}

// Apaga el sistema de forma segura
void Director::shutdownSystem(bool should_shutdown) {
    if (should_shutdown) {
        auto result = SystemShutdown::shutdownSystem(5, true);  // 5 segundos, forzar apps

        if (result != SystemShutdown::ShutdownResult::SUCCESS) {
            std::cerr << SystemShutdown::resultToString(result) << '\n';
        }
    }
}