// D:\Dev\Mosis\MosisService\designer\src\testing\ui_inspector.cpp
#include "ui_inspector.h"
#include <fstream>
#include <sstream>
#include <filesystem>

namespace mosis::testing {

using json = nlohmann::json;

// Helper to normalize RmlUi URLs for comparison
// RmlUi uses '|' instead of ':' for Windows drive letters
static std::string NormalizeUrl(const std::string& url) {
    std::string normalized = url;
    // Replace | with : (for Windows drive letters)
    for (char& c : normalized) {
        if (c == '|') c = ':';
    }
    // Normalize backslashes to forward slashes
    for (char& c : normalized) {
        if (c == '\\') c = '/';
    }
    return normalized;
}

UIInspector::UIInspector(Rml::Context* context)
    : m_context(context)
{
}

json UIInspector::ElementToJson(Rml::Element* element) const {
    if (!element) return nullptr;

    json j;
    j["tag"] = element->GetTagName();
    
    // ID
    std::string id = element->GetId();
    if (!id.empty()) {
        j["id"] = id;
    }
    
    // Classes - split into array
    std::string class_str = element->GetAttribute<Rml::String>("class", "");
    if (!class_str.empty()) {
        json classes_arr = json::array();
        std::istringstream iss(class_str);
        std::string cls;
        while (iss >> cls) {
            classes_arr.push_back(cls);
        }
        j["classes"] = classes_arr;
    }
    
    // Bounds
    auto box = element->GetAbsoluteOffset();
    auto size = element->GetBox().GetSize();
    j["bounds"] = {
        {"x", box.x},
        {"y", box.y},
        {"width", size.x},
        {"height", size.y}
    };
    
    // Visibility
    j["visible"] = element->IsVisible();
    
    // Text content (for leaf nodes)
    std::string text = element->GetInnerRML();
    // Only include if it's simple text (no child elements)
    if (element->GetNumChildren() == 0 && !text.empty()) {
        // Trim whitespace
        size_t start = text.find_first_not_of(" \t\n\r");
        size_t end = text.find_last_not_of(" \t\n\r");
        if (start != std::string::npos && end != std::string::npos) {
            j["text"] = text.substr(start, end - start + 1);
        }
    }
    
    // Children
    int num_children = element->GetNumChildren();
    if (num_children > 0) {
        j["children"] = json::array();
        for (int i = 0; i < num_children; i++) {
            auto* child = element->GetChild(i);
            if (child) {
                json child_json = ElementToJson(child);
                if (!child_json.is_null()) {
                    j["children"].push_back(child_json);
                }
            }
        }
    }
    
    return j;
}

json UIInspector::DumpHierarchy() const {
    json result;

    // Get current timestamp
    auto now = std::chrono::system_clock::now();
    auto time_t = std::chrono::system_clock::to_time_t(now);
    std::stringstream ss;
    ss << std::put_time(std::localtime(&time_t), "%Y-%m-%dT%H:%M:%S");
    result["timestamp"] = ss.str();

    // Resolution
    auto dimensions = m_context->GetDimensions();
    result["resolution"] = {
        {"width", dimensions.x},
        {"height", dimensions.y}
    };

    // Current screen - use override if set, otherwise detect from documents
    std::string screen = m_current_screen;
    Rml::ElementDocument* main_doc = nullptr;

    // Normalize the current screen path for comparison
    std::string normalized_current = NormalizeUrl(m_current_screen);

    // Find the document matching the current screen, or first non-debugger document
    for (int i = 0; i < m_context->GetNumDocuments(); i++) {
        auto* doc = m_context->GetDocument(i);
        if (doc) {
            std::string url = doc->GetSourceURL();
            // Skip debugger documents
            if (url.find("__rmlui") == std::string::npos) {
                // Normalize the document URL for comparison
                std::string normalized_url = NormalizeUrl(url);

                // If we have a current screen override, match it
                if (!m_current_screen.empty()) {
                    if (normalized_url.find(normalized_current) != std::string::npos ||
                        normalized_current.find(normalized_url) != std::string::npos) {
                        main_doc = doc;
                        break;
                    }
                } else if (!main_doc) {
                    // No override, use first non-debugger document
                    screen = url;
                    main_doc = doc;
                }
            }
        }
    }
    result["screen"] = screen;

    // Elements - dump the main document body (expected format for HierarchyReader)
    if (main_doc) {
        result["elements"] = ElementToJson(main_doc);
    } else {
        result["elements"] = json::object();
    }

    // Also include all documents for more detailed inspection
    result["documents"] = json::array();
    for (int i = 0; i < m_context->GetNumDocuments(); i++) {
        auto* doc = m_context->GetDocument(i);
        if (doc) {
            json doc_json;
            doc_json["url"] = doc->GetSourceURL();
            doc_json["title"] = doc->GetTitle();

            // Dump document root (ElementDocument inherits from Element)
            doc_json["body"] = ElementToJson(doc);

            result["documents"].push_back(doc_json);
        }
    }

    return result;
}

bool UIInspector::SaveHierarchy(const std::string& path) const {
    json hierarchy = DumpHierarchy();
    std::string content = hierarchy.dump(2);

    // Use temporary file + rename for atomic writes
    std::string temp_path = path + ".tmp";

    {
        std::ofstream file(temp_path, std::ios::binary | std::ios::trunc);
        if (!file) {
            return false;
        }
        file.write(content.c_str(), content.size());
        file.flush();
        if (!file.good()) {
            file.close();
            return false;
        }
        file.close();
    }

    // Delete the old file first to avoid rename conflicts on Windows
    std::error_code ec;
    std::filesystem::remove(path, ec);  // Ignore errors if file doesn't exist

    // Rename temp file to final path
    std::filesystem::rename(temp_path, path, ec);
    if (ec) {
        // Fallback: copy and delete if rename fails
        std::filesystem::copy_file(temp_path, path,
            std::filesystem::copy_options::overwrite_existing, ec);
        std::filesystem::remove(temp_path, ec);
    }

    return true;
}

Rml::Element* UIInspector::FindElementRecursive(Rml::Element* root, const std::string& selector) const {
    if (!root) return nullptr;
    
    // Check if this element matches
    // Simple selectors: #id, .class, tagname
    if (selector[0] == '#') {
        // ID selector
        std::string id = selector.substr(1);
        if (root->GetId() == id) return root;
    } else if (selector[0] == '.') {
        // Class selector
        std::string cls = selector.substr(1);
        std::string classes = root->GetAttribute<Rml::String>("class", "");
        if (classes.find(cls) != std::string::npos) return root;
    } else {
        // Tag name selector
        if (root->GetTagName() == selector) return root;
    }
    
    // Search children
    for (int i = 0; i < root->GetNumChildren(); i++) {
        auto* child = root->GetChild(i);
        auto* found = FindElementRecursive(child, selector);
        if (found) return found;
    }
    
    return nullptr;
}

Rml::Element* UIInspector::FindElement(const std::string& selector) const {
    for (int i = 0; i < m_context->GetNumDocuments(); i++) {
        auto* doc = m_context->GetDocument(i);
        if (doc) {
            // Document itself is the root element (inherits from Element)
            auto* found = FindElementRecursive(doc, selector);
            if (found) return found;
        }
    }
    return nullptr;
}

bool UIInspector::ElementExists(const std::string& selector) const {
    return FindElement(selector) != nullptr;
}

bool UIInspector::ElementVisible(const std::string& selector) const {
    auto* elem = FindElement(selector);
    return elem && elem->IsVisible();
}

UIInspector::Bounds UIInspector::GetElementBounds(const std::string& selector) const {
    Bounds bounds = {0, 0, 0, 0};
    auto* elem = FindElement(selector);
    if (elem) {
        auto box = elem->GetAbsoluteOffset();
        auto size = elem->GetBox().GetSize();
        bounds.x = box.x;
        bounds.y = box.y;
        bounds.width = size.x;
        bounds.height = size.y;
    }
    return bounds;
}

std::string UIInspector::GetElementText(const std::string& selector) const {
    auto* elem = FindElement(selector);
    if (elem) {
        return elem->GetInnerRML();
    }
    return "";
}

std::string UIInspector::GetElementAttribute(const std::string& selector, const std::string& attr) const {
    auto* elem = FindElement(selector);
    if (elem) {
        return elem->GetAttribute<Rml::String>(attr, "");
    }
    return "";
}

} // namespace mosis::testing
