// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3

#include "skin_manager.h"
#include "ui_schema.h"
#include "../../util/platform.h"
#include "../../resources/embedded_resources.h"
#include "../theme.h"
#include "../material/color_theme.h"
#include "../effects/theme_effects.h"
#include "../effects/imgui_acrylic.h"

#include <toml++/toml.hpp>
#include <fstream>
#include <filesystem>
#include <algorithm>
#include <cstdio>
#include "../../util/logger.h"

namespace fs = std::filesystem;

namespace dragonx {
namespace ui {
namespace schema {

// ============================================================================
// Singleton
// ============================================================================

SkinManager& SkinManager::instance() {
    static SkinManager s;
    return s;
}

// ============================================================================
// Directory helpers
// ============================================================================

std::string SkinManager::getBundledSkinsDirectory() {
    // Bundled skins live in res/themes/ next to the executable
    fs::path exe_dir = util::getExecutableDirectory();
    fs::path themes_dir = exe_dir / "res" / "themes";

    if (fs::exists(themes_dir)) {
        // Update any stale overlay themes from embedded versions
        int updated = resources::updateBundledThemes(themes_dir.string());
        if (updated > 0)
            DEBUG_LOGF("[SkinManager] Updated %d stale theme(s) in %s\n",
                   updated, themes_dir.string().c_str());
        return themes_dir.string();
    }

    // Fallback: current working directory
    themes_dir = fs::current_path() / "res" / "themes";
    if (fs::exists(themes_dir)) {
        return themes_dir.string();
    }

    // No on-disk themes dir found (single-file Windows distribution).
    // Extract embedded overlay themes to the config directory.
    fs::path configDir = util::Platform::getObsidianDragonDir();
    fs::path extractedDir = configDir / "bundled-themes";
    int extracted = resources::extractBundledThemes(extractedDir.string());
    if (extracted > 0) {
        DEBUG_LOGF("[SkinManager] Extracted %d embedded bundled themes to %s\n",
               extracted, extractedDir.string().c_str());
    }
    if (fs::exists(extractedDir)) {
        return extractedDir.string();
    }

    return (exe_dir / "res" / "themes").string();
}

std::string SkinManager::getUserSkinsDirectory() {
    // User themes in ObsidianDragon config directory — folder-based
    fs::path configDir = util::Platform::getObsidianDragonDir();
    return (configDir / "themes").string();
}

// ============================================================================
// Scan for skins
// ============================================================================

void SkinManager::scanDirectory(const std::string& dir, bool bundled) {
    if (!fs::exists(dir) || !fs::is_directory(dir)) {
        DEBUG_LOGF("[SkinManager] Directory does not exist: %s\n", dir.c_str());
        return;
    }

    DEBUG_LOGF("[SkinManager] Scanning %s directory: %s\n", bundled ? "bundled" : "user", dir.c_str());

    if (bundled) {
        // Bundled skins: all .toml files in res/themes/ except ui.toml
        // (ui.toml is the base theme and is always loaded as "dragonx")
        for (const auto& entry : fs::directory_iterator(dir)) {
            if (!entry.is_regular_file()) continue;

            fs::path p = entry.path();
            if (p.extension() != ".toml") continue;

            std::string stem = p.stem().string();

            // Skip ui.toml - it's the base theme handled separately as "dragonx"
            if (stem == "ui") continue;

            // Try to parse and extract metadata
            toml::table root;
            try {
                root = toml::parse_file(p.string());
            } catch (...) {
                DEBUG_LOGF("[SkinManager] Skipping '%s': invalid TOML\n", p.filename().string().c_str());
                continue;
            }

            auto* theme = root["theme"].as_table();
            if (!theme) {
                DEBUG_LOGF("[SkinManager] Skipping '%s': no [theme] section\n", p.filename().string().c_str());
                continue;
            }

            SkinInfo info;
            info.path = p.string();
            info.bundled = true;

            // ID = filename stem (e.g. "dark" from dark.toml)
            info.id = stem;

            if (auto name = (*theme)["name"].value<std::string>()) {
                info.name = *name;
            } else {
                info.name = info.id;
            }

            if (auto author = (*theme)["author"].value<std::string>()) {
                info.author = *author;
            }

            if (auto dark = (*theme)["dark"].value<bool>()) {
                info.dark = *dark;
            }

            // Resolve image paths from theme.images (bundled: res/img/)
            fs::path imgDir = p.parent_path().parent_path() / "img";
            std::string bgFilename;
            std::string logoFilename;

            if (auto* images = (*theme)["images"].as_table()) {
                if (auto bg = (*images)["background_image"].value<std::string>()) {
                    bgFilename = *bg;
                }
                if (auto logo = (*images)["logo"].value<std::string>()) {
                    logoFilename = *logo;
                }
            }

            if (!bgFilename.empty()) {
                fs::path bgPath = imgDir / bgFilename;
                if (fs::exists(bgPath)) {
                    info.backgroundImagePath = bgPath.string();
                }
            }

            if (!logoFilename.empty()) {
                fs::path logoImgPath = imgDir / logoFilename;
                if (fs::exists(logoImgPath)) {
                    info.logoPath = logoImgPath.string();
                }
            }

            skins_.push_back(std::move(info));
        }
    } else {
        // User themes: each subfolder must contain a theme.toml
        for (const auto& entry : fs::directory_iterator(dir)) {
            if (!entry.is_directory()) continue;

            fs::path themeDir = entry.path();
            fs::path themeToml = themeDir / "theme.toml";

            if (!fs::exists(themeToml)) {
                DEBUG_LOGF("[SkinManager] Skipping folder '%s': no theme.toml found\n",
                       themeDir.filename().string().c_str());
                continue;
            }

            DEBUG_LOGF("[SkinManager] Found theme folder: %s (theme.toml exists)\n", themeDir.filename().string().c_str());

            // Validate the theme file
            auto validation = validateSkinFile(themeToml.string());

            // Parse metadata even from invalid themes (so they show in the list)
            toml::table root;
            try {
                root = toml::parse_file(themeToml.string());
            } catch (...) {
                // Still add as invalid
                SkinInfo info;
                info.id = themeDir.filename().string();
                info.name = info.id;
                info.path = themeToml.string();
                info.directory = themeDir.string();
                info.bundled = false;
                info.valid = false;
                info.validationError = "Invalid TOML";
                skins_.push_back(std::move(info));
                continue;
            }

            SkinInfo info;
            info.id = themeDir.filename().string();
            info.path = themeToml.string();
            info.directory = themeDir.string();
            info.bundled = false;
            info.valid = validation.valid;
            info.validationError = validation.error;

            // Extract metadata from theme section
            if (auto* theme = root["theme"].as_table()) {

                if (auto name = (*theme)["name"].value<std::string>()) {
                    info.name = *name;
                } else {
                    info.name = info.id;
                }

                if (auto author = (*theme)["author"].value<std::string>()) {
                    info.author = *author;
                }

                if (auto dark = (*theme)["dark"].value<bool>()) {
                    info.dark = *dark;
                }

                // Resolve image paths (from TOML)
                fs::path imgDir = themeDir / "img";
                std::string bgFilename;
                std::string logoFilename;

                if (auto* images = (*theme)["images"].as_table()) {
                    if (auto bg = (*images)["background_image"].value<std::string>()) {
                        bgFilename = *bg;
                    }
                    if (auto logo = (*images)["logo"].value<std::string>()) {
                        logoFilename = *logo;
                    }
                }

                // Check if image files exist
                if (!bgFilename.empty()) {
                    fs::path bgPath = imgDir / bgFilename;
                    if (fs::exists(bgPath)) {
                        info.backgroundImagePath = bgPath.string();
                    }
                }

                if (!logoFilename.empty()) {
                    fs::path logoImgPath = imgDir / logoFilename;
                    if (fs::exists(logoImgPath)) {
                        info.logoPath = logoImgPath.string();
                    }
                }
            } else {
                info.name = info.id;
            }

            skins_.push_back(std::move(info));
        }

        // Also scan for loose .toml files (unified format with [theme.palette])
        for (const auto& entry : fs::directory_iterator(dir)) {
            if (!entry.is_regular_file()) continue;

            fs::path p = entry.path();
            if (p.extension() != ".toml") continue;

            toml::table root;
            try {
                root = toml::parse_file(p.string());
            } catch (...) {
                DEBUG_LOGF("[SkinManager] Skipping '%s': invalid TOML\n", p.filename().string().c_str());
                continue;
            }

            SkinInfo info;
            info.path = p.string();
            info.id = p.stem().string();
            info.bundled = false;

            // Check for unified format ([theme] with [theme.palette])
            auto* theme = root["theme"].as_table();
            if (theme) {
                if (auto name = (*theme)["name"].value<std::string>()) {
                    info.name = *name;
                } else {
                    info.name = info.id;
                }
                if (auto author = (*theme)["author"].value<std::string>()) {
                    info.author = *author;
                }
                if (auto dark = (*theme)["dark"].value<bool>()) {
                    info.dark = *dark;
                }

                auto validation = validateSkinFile(p.string());
                info.valid = validation.valid;
                info.validationError = validation.error;

                // Resolve image paths (look in same directory as the .toml file)
                fs::path imgDir = p.parent_path();
                std::string bgFilename;
                std::string logoFilename;

                if (auto* images = (*theme)["images"].as_table()) {
                    if (auto bg = (*images)["background_image"].value<std::string>()) {
                        bgFilename = *bg;
                    }
                    if (auto logo = (*images)["logo"].value<std::string>()) {
                        logoFilename = *logo;
                    }
                }

                if (!bgFilename.empty()) {
                    fs::path bgPath = imgDir / bgFilename;
                    if (fs::exists(bgPath)) {
                        info.backgroundImagePath = bgPath.string();
                    }
                }
                if (!logoFilename.empty()) {
                    fs::path logoImgPath = imgDir / logoFilename;
                    if (fs::exists(logoImgPath)) {
                        info.logoPath = logoImgPath.string();
                    }
                }
            }
            else {
                DEBUG_LOGF("[SkinManager] Skipping '%s': unrecognized TOML format\n",
                       p.filename().string().c_str());
                continue;
            }

            skins_.push_back(std::move(info));
        }
    }
}

void SkinManager::refresh() {
    skins_.clear();

    // Scan bundled skins (res/ directory)
    scanDirectory(getBundledSkinsDirectory(), true);

    // Scan user skins
    std::string userDir = getUserSkinsDirectory();
    if (fs::exists(userDir)) {
        scanDirectory(userDir, false);
    }

    // Ensure the base "dragonx" theme always appears (it's ui.toml, the main theme).
    // Other bundled themes are discovered automatically from res/themes/*.toml.
    {
        bool found = false;
        for (const auto& s : skins_) {
            if (s.id == "dragonx") { found = true; break; }
        }
        if (!found) {
            SkinInfo info;
            info.id      = "dragonx";
            info.name    = "DragonX";
            info.author  = "The Hush Developers";
            info.dark    = true;
            info.bundled = true;
            info.valid   = true;
            // Try to set path to ui.toml if it exists
            fs::path uiPath = fs::path(getBundledSkinsDirectory()) / "ui.toml";
            if (fs::exists(uiPath)) {
                info.path = uiPath.string();
            }
            skins_.push_back(std::move(info));
            DEBUG_LOGF("[SkinManager] Injected base theme: dragonx\n");
        }
    }

    // Sort: "dragonx" first, then bundled grouped by mode (dark then light), then user
    std::sort(skins_.begin(), skins_.end(), [](const SkinInfo& a, const SkinInfo& b) {
        // DragonX always first
        if (a.id == "dragonx") return true;
        if (b.id == "dragonx") return false;

        // Bundled before user
        if (a.bundled != b.bundled) return a.bundled;

        // Group: dark themes first, then light themes
        if (a.dark != b.dark) return a.dark;

        // Alphabetical by name within each group
        return a.name < b.name;
    });

    DEBUG_LOGF("[SkinManager] Found %zu skins\n", skins_.size());
}

// ============================================================================
// Find
// ============================================================================

const SkinManager::SkinInfo* SkinManager::findById(const std::string& id) const {
    for (const auto& skin : skins_) {
        if (skin.id == id) return &skin;
    }
    return nullptr;
}

// ============================================================================
// Validation
// ============================================================================

SkinManager::ValidationResult SkinManager::validateSkinFile(const std::string& path) {
    ValidationResult result;

    // 1. Must be valid TOML
    toml::table root;
    try {
        root = toml::parse_file(path);
    } catch (const toml::parse_error& e) {
        result.error = std::string("Invalid TOML: ") + e.what();
        return result;
    }

    // 3. Must contain "theme" table
    auto* theme = root["theme"].as_table();
    if (!theme) {
        result.error = "Missing or invalid 'theme' section";
        return result;
    }

    // 4. theme.name must be a non-empty string
    auto name = (*theme)["name"].value<std::string>();
    if (!name || name->empty()) {
        result.error = "theme.name must be a non-empty string";
        return result;
    }

    // 5. theme.palette must exist with at least --primary and --background
    auto* palette = (*theme)["palette"].as_table();
    if (!palette) {
        result.error = "Missing theme.palette table";
        return result;
    }

    if (!palette->contains("--primary")) {
        result.error = "Palette missing required '--primary' color";
        return result;
    }
    if (!palette->contains("--background")) {
        result.error = "Palette missing required '--background' color";
        return result;
    }

    // 6. If globals exists, must be a table
    if (root.contains("globals") && !root["globals"].is_table()) {
        result.error = "'globals' must be a table";
        return result;
    }

    result.valid = true;
    return result;
}

// ============================================================================
// Import
// ============================================================================

bool SkinManager::importSkin(const std::string& sourcePath) {
    fs::path srcPath(sourcePath);
    std::string userDir = getUserSkinsDirectory();

    try {
        fs::create_directories(userDir);
    } catch (const fs::filesystem_error& e) {
        DEBUG_LOGF("[SkinManager] Failed to create themes directory: %s\n", e.what());
        return false;
    }

    if (fs::is_directory(srcPath)) {
        // Import a theme folder — copy entire folder into themes/
        fs::path themeToml = srcPath / "theme.toml";
        if (!fs::exists(themeToml)) {
            DEBUG_LOGF("[SkinManager] Import folder has no theme.toml: %s\n", sourcePath.c_str());
            return false;
        }

        auto validation = validateSkinFile(themeToml.string());
        if (!validation.valid) {
            DEBUG_LOGF("[SkinManager] Import validation failed: %s\n", validation.error.c_str());
            return false;
        }

        fs::path destDir = fs::path(userDir) / srcPath.filename();
        try {
            fs::copy(srcPath, destDir, fs::copy_options::recursive | fs::copy_options::overwrite_existing);
        } catch (const fs::filesystem_error& e) {
            DEBUG_LOGF("[SkinManager] Failed to copy theme folder: %s\n", e.what());
            return false;
        }

        DEBUG_LOGF("[SkinManager] Imported theme folder: %s → %s\n", sourcePath.c_str(), destDir.string().c_str());
    } else {
        // Import a single .toml file — create a folder for it
        auto validation = validateSkinFile(sourcePath);
        if (!validation.valid) {
            DEBUG_LOGF("[SkinManager] Import validation failed: %s\n", validation.error.c_str());
            return false;
        }

        std::string folderName = srcPath.stem().string();
        fs::path destDir = fs::path(userDir) / folderName;
        try {
            fs::create_directories(destDir);
            fs::copy_file(srcPath, destDir / "theme.toml", fs::copy_options::overwrite_existing);
        } catch (const fs::filesystem_error& e) {
            DEBUG_LOGF("[SkinManager] Failed to import skin file: %s\n", e.what());
            return false;
        }

        DEBUG_LOGF("[SkinManager] Imported skin file as folder: %s → %s\n", sourcePath.c_str(), destDir.string().c_str());
    }

    refresh();
    return true;
}

// ============================================================================
// Remove
// ============================================================================

bool SkinManager::removeSkin(const std::string& id) {
    const SkinInfo* skin = findById(id);
    if (!skin) {
        DEBUG_LOGF("[SkinManager] Skin not found: %s\n", id.c_str());
        return false;
    }

    if (skin->bundled) {
        DEBUG_LOGF("[SkinManager] Cannot remove bundled skin: %s\n", id.c_str());
        return false;
    }

    try {
        if (!skin->directory.empty() && fs::is_directory(skin->directory)) {
            // Folder-based theme — remove the entire directory
            fs::remove_all(skin->directory);
        } else {
            // Legacy flat file
            fs::remove(skin->path);
        }
    } catch (const fs::filesystem_error& e) {
        DEBUG_LOGF("[SkinManager] Failed to remove skin: %s\n", e.what());
        return false;
    }

    DEBUG_LOGF("[SkinManager] Removed skin: %s\n", id.c_str());

    // If we removed the active skin, fall back to default
    if (activeSkinId_ == id) {
        setActiveSkin("dragonx");
    }

    refresh();
    return true;
}

// ============================================================================
// Activate skin
// ============================================================================

bool SkinManager::setActiveSkin(const std::string& id) {
    const SkinInfo* skin = findById(id);
    if (!skin) {
        DEBUG_LOGF("[SkinManager] Skin not found: %s\n", id.c_str());
        return false;
    }

    if (!skin->valid) {
        DEBUG_LOGF("[SkinManager] Skin is invalid: %s (%s)\n", id.c_str(), skin->validationError.c_str());
        return false;
    }

    bool loaded = false;

    // For skin files: always reload base layout first, then merge visual
    // overlay on top. This ensures overlays only change palette + backdrop
    // while inheriting all layout values from ui.toml.
    if (!skin->path.empty()) {
        auto& schema = UISchema::instance();
        std::string basePath = schema.basePath();

        if (!basePath.empty() || schema.hasEmbeddedBase()) {
            if (!basePath.empty() && basePath == skin->path) {
                // Switching back to the base theme: full reload, clear overlay
                schema.reloadBase();
                schema.reapplyColorsToImGui();
                loaded = true;
            } else {
                // Switching to a non-base skin: reload base then merge overlay
                if (schema.reloadBase()) {
                    if (schema.mergeOverlayFromFile(skin->path)) {
                        schema.reapplyColorsToImGui();
                        loaded = true;
                    }
                }
            }
        }

        // Fallback: no base path or embedded data, full load of skin file
        if (!loaded && schema.loadFromFile(skin->path)) {
            schema.reapplyColorsToImGui();
            loaded = true;
        }
    } else if (!id.empty()) {
        // Skin with no path (e.g., "dragonx" on Windows with embedded ui.toml):
        // just reload the base to restore the default theme
        auto& schema = UISchema::instance();
        if (schema.hasEmbeddedBase() || !schema.basePath().empty()) {
            schema.reloadBase();
            schema.reapplyColorsToImGui();
            loaded = true;
        }
    }

    // Fall back to built-in C++ themes (works even without theme files)
    if (!loaded) {
        if (!SetThemeById(id)) {
            DEBUG_LOGF("[SkinManager] Failed to load skin: %s\n", id.c_str());
            return false;
        }
        DEBUG_LOGF("[SkinManager] Loaded via built-in theme fallback: %s\n", id.c_str());
        loaded = true;
    }

    activeSkinId_ = id;
    DEBUG_LOGF("[SkinManager] Activated skin: %s (%s)\n", id.c_str(), skin->name.c_str());

    // Reload theme visual effects config from the new skin's [effects] section
    effects::ThemeEffects::instance().loadFromTheme();

    // Resolve image paths from UISchema (which parsed [theme] images from the TOML).
    // The UISchema stores relative filenames (e.g. "backgrounds/texture/drgx_bg.png");
    // resolve them to absolute paths using the theme's directory structure.
    {
        // Use a mutable reference to update the SkinInfo
        SkinInfo* mutableSkin = nullptr;
        for (auto& s : skins_) {
            if (s.id == id) { mutableSkin = &s; break; }
        }

        fs::path imgDir = resolveImgDir(skin);
        resolveAndFireCallback(mutableSkin, imgDir);
    }

    // Force acrylic to re-capture the background with new theme colors/images.
    // This must happen AFTER images are reloaded so the next frame renders the
    // updated background before capture.
    effects::ImGuiAcrylic::InvalidateCapture();

    return true;
}

void SkinManager::resolveAndReloadImages(const std::string& skinId, const std::string& tomlPath) {
    // Find the skin and update its image paths from the current UISchema values
    SkinInfo* skin = nullptr;
    for (auto& s : skins_) {
        if (s.id == skinId) {
            skin = &s;
            break;
        }
    }

    fs::path imgDir = resolveImgDir(skin);
    resolveAndFireCallback(skin, imgDir);
}

// ---------------------------------------------------------------------------
// Gradient mode
// ---------------------------------------------------------------------------

void SkinManager::setGradientMode(bool enabled) {
    if (gradientMode_ == enabled) return;
    gradientMode_ = enabled;

    // Re-resolve + reload for the currently active skin
    const SkinInfo* skin = findById(activeSkinId_);
    if (!skin) return;

    SkinInfo* mutableSkin = nullptr;
    for (auto& s : skins_) {
        if (s.id == activeSkinId_) { mutableSkin = &s; break; }
    }

    fs::path imgDir = resolveImgDir(skin);
    resolveAndFireCallback(mutableSkin, imgDir);

    effects::ImGuiAcrylic::InvalidateCapture();
}

// ---------------------------------------------------------------------------
// Private helpers
// ---------------------------------------------------------------------------

fs::path SkinManager::resolveImgDir(const SkinInfo* skin) const {
    if (!skin) {
        return fs::path(getBundledSkinsDirectory()).parent_path() / "img";
    }
    if (skin->bundled) {
        fs::path themeDir = skin->path.empty()
            ? fs::path(getBundledSkinsDirectory())
            : fs::path(skin->path).parent_path();
        return themeDir.parent_path() / "img";
    }
    if (!skin->directory.empty()) {
        return fs::path(skin->directory) / "img";
    }
    if (!skin->path.empty()) {
        return fs::path(skin->path).parent_path();
    }
    return fs::path(getBundledSkinsDirectory()).parent_path() / "img";
}

std::string SkinManager::resolveGradientBg(const std::string& bgFilename,
                                            const fs::path& imgDir,
                                            bool isDark) const {
    // Given bgFilename like "backgrounds/texture/drgx_bg.png",
    // look for "backgrounds/gradient/gradient_drgx_bg.png".
    fs::path bgRel(bgFilename);
    std::string stem = bgRel.stem().string();       // "drgx_bg"
    std::string ext  = bgRel.extension().string();  // ".png"

    // Build the gradient candidate: backgrounds/gradient/gradient_<stem><ext>
    std::string gradientRel = "backgrounds/gradient/gradient_" + stem + ext;
    fs::path gradientPath = imgDir / gradientRel;

    if (fs::exists(gradientPath)) {
        return gradientPath.string();
    }

    // Fallback: dark_gradient.png or light_gradient.png
    std::string fallbackName = isDark ? "dark_gradient.png" : "light_gradient.png";
    std::string fallbackRel = "backgrounds/gradient/" + fallbackName;
    fs::path fallbackPath = imgDir / fallbackRel;

    if (fs::exists(fallbackPath)) {
        return fallbackPath.string();
    }

    // Last resort: pass the relative gradient filename for embedded lookup (Windows)
    return gradientRel;
}

void SkinManager::resolveAndFireCallback(SkinInfo* skin, const fs::path& imgDir) {
    auto& schema = UISchema::instance();
    std::string bgFilename  = schema.backgroundImagePath();
    std::string logoFilename = schema.logoImagePath();

    std::string resolvedBg;
    std::string resolvedLogo;

    if (!bgFilename.empty()) {
        if (gradientMode_) {
            resolvedBg = resolveGradientBg(bgFilename, imgDir, schema.isDarkTheme());
        } else {
            fs::path bgPath = imgDir / bgFilename;
            if (fs::exists(bgPath)) {
                resolvedBg = bgPath.string();
            } else {
                resolvedBg = bgFilename;
            }
        }
    }

    if (!logoFilename.empty()) {
        fs::path logoPath = imgDir / logoFilename;
        if (fs::exists(logoPath)) {
            resolvedLogo = logoPath.string();
        } else {
            resolvedLogo = logoFilename;
        }
    }

    if (skin) {
        skin->backgroundImagePath = resolvedBg;
        skin->logoPath = resolvedLogo;
    }

    DEBUG_LOGF("[SkinManager] Resolved images (gradient=%s): bg='%s', logo='%s'\n",
           gradientMode_ ? "on" : "off", resolvedBg.c_str(), resolvedLogo.c_str());

    if (imageReloadCb_) {
        imageReloadCb_(resolvedBg, resolvedLogo);
    }
}

} // namespace schema
} // namespace ui
} // namespace dragonx
