#include "project_manager.h"

#include "../../../../cpp/framework/app_base.h"

#include <sqlite3.h>

#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <sstream>

#if defined(_WIN32)
#  include <windows.h>
#  include <shellapi.h>
#endif

namespace fs = std::filesystem;

namespace ge {

// Helpers de paths — resuelven contra <exe_dir>/local_files/.
const char* projects_root() {
    return fn::local_path(k_projects_subdir);
}
const char* settings_path() {
    return fn::local_path(k_settings_basename);
}

// ----------------------------------------------------------------------------
// DDL embebido (operations.db schema)
// ----------------------------------------------------------------------------

static const char* k_operations_db_ddl = R"SQL(
CREATE TABLE IF NOT EXISTS types_snapshot (
    id          TEXT PRIMARY KEY,
    version     TEXT NOT NULL DEFAULT '1.0.0',
    lang        TEXT NOT NULL,
    algebraic   TEXT NOT NULL CHECK(algebraic IN ('product','sum')),
    definition  TEXT NOT NULL DEFAULT '',
    description TEXT NOT NULL DEFAULT '',
    snapped_at  TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS entities (
    id          TEXT PRIMARY KEY,
    name        TEXT NOT NULL,
    type_ref    TEXT NOT NULL,
    status      TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active','stale','corrupted','archived')),
    description TEXT NOT NULL DEFAULT '',
    domain      TEXT NOT NULL DEFAULT '',
    tags        TEXT NOT NULL DEFAULT '[]',
    source      TEXT NOT NULL DEFAULT 'graph_explorer',
    metadata    TEXT NOT NULL DEFAULT '{}',
    notes       TEXT NOT NULL DEFAULT '',
    group_id    TEXT,
    created_at  TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now')),
    updated_at  TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now'))
);
CREATE TABLE IF NOT EXISTS relations (
    id          TEXT PRIMARY KEY,
    name        TEXT NOT NULL,
    from_entity TEXT NOT NULL DEFAULT '',
    to_entity   TEXT NOT NULL,
    via         TEXT NOT NULL DEFAULT '',
    description TEXT NOT NULL DEFAULT '',
    purity      TEXT NOT NULL DEFAULT '' CHECK(purity IN ('','pure','impure')),
    direction   TEXT NOT NULL DEFAULT 'unidirectional' CHECK(direction IN ('unidirectional','bidirectional','inverse')),
    weight      REAL,
    status      TEXT NOT NULL DEFAULT 'designed' CHECK(status IN ('designed','implemented','tested','running','deprecated')),
    started_at  TEXT,
    ended_at    TEXT,
    "order"     INTEGER,
    tags        TEXT NOT NULL DEFAULT '[]',
    notes       TEXT NOT NULL DEFAULT '',
    created_at  TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now')),
    updated_at  TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now'))
);
CREATE TABLE IF NOT EXISTS relation_inputs (
    id          TEXT PRIMARY KEY,
    relation_id TEXT NOT NULL REFERENCES relations(id) ON DELETE CASCADE,
    entity_id   TEXT NOT NULL REFERENCES entities(id),
    role        TEXT NOT NULL,
    "order"     INTEGER
);
CREATE VIRTUAL TABLE IF NOT EXISTS entities_fts USING fts5(
    id, name, description, tags, domain,
    content='entities', content_rowid='rowid'
);
CREATE TRIGGER IF NOT EXISTS entities_ai AFTER INSERT ON entities BEGIN
    INSERT INTO entities_fts(rowid, id, name, description, tags, domain)
    VALUES (new.rowid, new.id, new.name, new.description, new.tags, new.domain);
END;
CREATE TRIGGER IF NOT EXISTS entities_ad AFTER DELETE ON entities BEGIN
    INSERT INTO entities_fts(entities_fts, rowid, id, name, description, tags, domain)
    VALUES ('delete', old.rowid, old.id, old.name, old.description, old.tags, old.domain);
END;
CREATE TRIGGER IF NOT EXISTS entities_au AFTER UPDATE ON entities BEGIN
    INSERT INTO entities_fts(entities_fts, rowid, id, name, description, tags, domain)
    VALUES ('delete', old.rowid, old.id, old.name, old.description, old.tags, old.domain);
    INSERT INTO entities_fts(rowid, id, name, description, tags, domain)
    VALUES (new.rowid, new.id, new.name, new.description, new.tags, new.domain);
END;
CREATE TABLE IF NOT EXISTS assertions (
    id TEXT PRIMARY KEY, entity_id TEXT NOT NULL, name TEXT NOT NULL,
    kind TEXT NOT NULL, rule TEXT NOT NULL,
    severity TEXT NOT NULL DEFAULT 'warning',
    description TEXT DEFAULT '', active INTEGER DEFAULT 1,
    created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now'))
);
CREATE TABLE IF NOT EXISTS assertion_results (
    id TEXT PRIMARY KEY, assertion_id TEXT NOT NULL,
    execution_id TEXT DEFAULT '', status TEXT NOT NULL,
    value TEXT DEFAULT '{}', message TEXT DEFAULT '',
    evaluated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now'))
);
CREATE TABLE IF NOT EXISTS executions (
    id TEXT PRIMARY KEY, pipeline_id TEXT NOT NULL,
    relation_id TEXT DEFAULT '', status TEXT NOT NULL,
    started_at TEXT NOT NULL, ended_at TEXT DEFAULT '',
    duration_ms INTEGER DEFAULT 0,
    records_in INTEGER DEFAULT 0, records_out INTEGER DEFAULT 0,
    error TEXT DEFAULT '', metrics TEXT DEFAULT '{}',
    created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now'))
);
CREATE TABLE IF NOT EXISTS logs (
    id TEXT PRIMARY KEY, level TEXT NOT NULL DEFAULT 'info',
    source TEXT DEFAULT '', entity_id TEXT DEFAULT '',
    execution_id TEXT DEFAULT '', message TEXT NOT NULL,
    metadata TEXT DEFAULT '{}',
    created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now'))
);
)SQL";

// Semilla types.yaml minima si examples/types.yaml no se encuentra. Incluye
// `fields` (issue 0005) y el tipo Table como cuadrado (issue 0010 preview).
static const char* k_seed_types_yaml = R"YAML(# types.yaml — generado al crear el proyecto. Editable desde el Type Editor.
# Todos los nodos son circulo, salvo "Table" que es cuadrado.
# Tipos de campo: string | int | float | bool | date | url | enum.

entities:
  - name: Person
    color: "#5B8DEF"
    icon: ti-user
    principal_field: name
    fields:
      - { name: name,       type: string, required: true }
      - { name: first_name, type: string }
      - { name: last_name,  type: string }

  - name: Email
    color: "#58CA8C"
    icon: ti-mail
    principal_field: address
    fields:
      - { name: address, type: string, required: true }

  - name: Domain
    color: "#F4B860"
    icon: ti-world

  - name: Org
    color: "#C780E8"
    icon: ti-building

  - name: Document
    color: "#C9C9C9"
    icon: ti-file

  - name: Table
    color: "#0EA5E9"
    icon: ti-database
    principal_field: name
    fields:
      - { name: name,     type: string, required: true }
      - { name: row_type, type: string }

relations:
  - name: owns
    color: "#888888"
    style: solid

  - name: knows
    color: "#AAAAAA"
    style: solid

  - name: located_in
    color: "#FFB870"
    style: dashed

  - name: CONTAINS_ROW
    color: "#0EA5E9"
    style: dotted
)YAML";

// ----------------------------------------------------------------------------
// Slug / paths
// ----------------------------------------------------------------------------

bool project_validate_slug(const char* name, std::string* error_msg) {
    if (!name || !*name) {
        if (error_msg) *error_msg = "slug vacio";
        return false;
    }
    size_t n = std::strlen(name);
    if (n > 64) {
        if (error_msg) *error_msg = "slug demasiado largo (max 64)";
        return false;
    }
    for (size_t i = 0; i < n; ++i) {
        char c = name[i];
        bool ok = (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9')
                  || c == '_' || c == '-';
        if (!ok) {
            if (error_msg) {
                *error_msg = "caracter no permitido (use a-z, 0-9, _ , -): '";
                error_msg->push_back(c);
                error_msg->push_back('\'');
            }
            return false;
        }
    }
    return true;
}

ProjectPaths project_paths(const char* slug) {
    ProjectPaths p;
    if (!slug || !*slug) return p;
    fs::path root = fs::path(projects_root()) / slug;
    p.root_dir      = root.string();
    p.operations_db = (root / "operations.db").string();
    p.types_yaml    = (root / "types.yaml").string();
    p.layout_db     = (root / "graph_explorer.db").string();
    return p;
}

// ----------------------------------------------------------------------------
// Listing / existence
// ----------------------------------------------------------------------------

bool projects_root_ensure() {
    std::error_code ec;
    fs::create_directories(projects_root(), ec);
    return !ec;
}

bool project_list(std::vector<std::string>* out) {
    if (!out) return false;
    out->clear();
    std::error_code ec;
    if (!fs::exists(projects_root(), ec)) return true;
    for (auto& e : fs::directory_iterator(projects_root(), ec)) {
        if (ec) break;
        if (!e.is_directory()) continue;
        std::string slug = e.path().filename().string();
        std::string err;
        if (!project_validate_slug(slug.c_str(), &err)) continue;
        out->push_back(slug);
    }
    std::sort(out->begin(), out->end());
    return true;
}

bool project_exists(const char* slug) {
    if (!slug || !*slug) return false;
    ProjectPaths p = project_paths(slug);
    std::error_code ec;
    return fs::exists(p.operations_db, ec);
}

// ----------------------------------------------------------------------------
// Bootstrap operations.db con el DDL embebido
// ----------------------------------------------------------------------------

static bool bootstrap_operations_db(const std::string& path, std::string* error_msg) {
    sqlite3* db = nullptr;
    int rc = sqlite3_open_v2(path.c_str(), &db,
                              SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, nullptr);
    if (rc != SQLITE_OK) {
        if (error_msg) {
            *error_msg = "sqlite3_open failed: ";
            *error_msg += db ? sqlite3_errmsg(db) : sqlite3_errstr(rc);
        }
        if (db) sqlite3_close(db);
        return false;
    }
    char* errmsg = nullptr;
    rc = sqlite3_exec(db, k_operations_db_ddl, nullptr, nullptr, &errmsg);
    if (rc != SQLITE_OK) {
        if (error_msg) {
            *error_msg = "DDL exec failed: ";
            *error_msg += errmsg ? errmsg : "(unknown)";
        }
        if (errmsg) sqlite3_free(errmsg);
        sqlite3_close(db);
        return false;
    }
    sqlite3_close(db);
    return true;
}

// ----------------------------------------------------------------------------
// Migraciones idempotentes de operations.db existente
// ----------------------------------------------------------------------------
//
// Cada migracion es un check + ALTER TABLE / CREATE ... condicional. Se
// ejecuta una vez al abrir un proyecto (project_migrate_schema). Detecta el
// estado actual via PRAGMA table_info y ejecuta solo lo que falta. No-op si
// la BD ya esta al dia.
//
// Issue 0035a: anade columna entities.group_id (TEXT NULL) si no existe.

// Devuelve true si la columna `column` existe en la tabla `table`.
static bool table_has_column(sqlite3* db, const char* table, const char* column) {
    std::string sql = "PRAGMA table_info(";
    sql += table;
    sql += ");";
    sqlite3_stmt* stmt = nullptr;
    if (sqlite3_prepare_v2(db, sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK) {
        return false;
    }
    bool found = false;
    while (sqlite3_step(stmt) == SQLITE_ROW) {
        const unsigned char* name = sqlite3_column_text(stmt, 1);
        if (name && std::strcmp(reinterpret_cast<const char*>(name), column) == 0) {
            found = true;
            break;
        }
    }
    sqlite3_finalize(stmt);
    return found;
}

bool project_migrate_schema(const std::string& path, std::string* error_msg) {
    sqlite3* db = nullptr;
    int rc = sqlite3_open_v2(path.c_str(), &db,
                              SQLITE_OPEN_READWRITE, nullptr);
    if (rc != SQLITE_OK) {
        if (error_msg) {
            *error_msg = "sqlite3_open failed: ";
            *error_msg += db ? sqlite3_errmsg(db) : sqlite3_errstr(rc);
        }
        if (db) sqlite3_close(db);
        return false;
    }

    // 0035a: entities.group_id
    if (!table_has_column(db, "entities", "group_id")) {
        char* errmsg = nullptr;
        rc = sqlite3_exec(db,
            "ALTER TABLE entities ADD COLUMN group_id TEXT",
            nullptr, nullptr, &errmsg);
        if (rc != SQLITE_OK) {
            if (error_msg) {
                *error_msg = "ALTER TABLE entities ADD COLUMN group_id failed: ";
                *error_msg += errmsg ? errmsg : "(unknown)";
            }
            if (errmsg) sqlite3_free(errmsg);
            sqlite3_close(db);
            return false;
        }
        if (errmsg) sqlite3_free(errmsg);
        std::fprintf(stdout,
            "[project_manager] migrated %s: ALTER TABLE entities ADD COLUMN group_id\n",
            path.c_str());
    }

    sqlite3_close(db);
    return true;
}

// ----------------------------------------------------------------------------
// project_create
// ----------------------------------------------------------------------------

bool project_create(const char* slug, std::string* error_msg) {
    std::string err;
    if (!project_validate_slug(slug, &err)) {
        if (error_msg) *error_msg = err;
        return false;
    }
    if (project_exists(slug)) {
        if (error_msg) *error_msg = "el proyecto ya existe";
        return false;
    }
    if (!projects_root_ensure()) {
        if (error_msg) *error_msg = "no se pudo crear el directorio projects/";
        return false;
    }

    ProjectPaths p = project_paths(slug);
    std::error_code ec;
    fs::create_directories(p.root_dir, ec);
    if (ec) {
        if (error_msg) *error_msg = "mkdir falló: " + ec.message();
        return false;
    }

    if (!bootstrap_operations_db(p.operations_db, error_msg)) {
        return false;
    }

    // types.yaml: copia desde examples/types.yaml si existe, si no usa embed.
    std::error_code ec2;
    bool wrote_yaml = false;
    if (fs::exists("examples/types.yaml", ec2)) {
        fs::copy_file("examples/types.yaml", p.types_yaml,
                       fs::copy_options::overwrite_existing, ec2);
        if (!ec2) wrote_yaml = true;
    }
    if (!wrote_yaml) {
        std::ofstream out(p.types_yaml);
        if (!out) {
            if (error_msg) *error_msg = "no se pudo escribir types.yaml";
            return false;
        }
        out << k_seed_types_yaml;
    }
    return true;
}

// ----------------------------------------------------------------------------
// Migracion legacy (mover operations.db del cwd a projects/default/)
// ----------------------------------------------------------------------------

bool projects_migrate_legacy_layout() {
    std::error_code ec;
    if (fs::exists(projects_root(), ec)) return true; // ya migrado o creado

    bool has_operations = fs::exists("operations.db", ec);
    bool has_layout     = fs::exists("graph_explorer.db", ec);
    if (!has_operations && !has_layout) return true; // nada que migrar

    if (!projects_root_ensure()) return false;
    ProjectPaths p = project_paths(k_default_project);
    fs::create_directories(p.root_dir, ec);
    if (ec) return false;

    if (has_operations) {
        fs::rename("operations.db", p.operations_db, ec);
        if (ec) {
            // fallback: copia + borra
            fs::copy_file("operations.db", p.operations_db,
                           fs::copy_options::overwrite_existing, ec);
            if (!ec) fs::remove("operations.db", ec);
        }
    }
    if (has_layout) {
        std::error_code ec3;
        fs::rename("graph_explorer.db", p.layout_db, ec3);
        if (ec3) {
            fs::copy_file("graph_explorer.db", p.layout_db,
                           fs::copy_options::overwrite_existing, ec3);
            if (!ec3) fs::remove("graph_explorer.db", ec3);
        }
    }
    // semilla types.yaml para el proyecto default
    if (!fs::exists(p.types_yaml, ec)) {
        std::error_code ec4;
        if (fs::exists("examples/types.yaml", ec4)) {
            fs::copy_file("examples/types.yaml", p.types_yaml,
                           fs::copy_options::overwrite_existing, ec4);
        } else {
            std::ofstream out(p.types_yaml);
            if (out) out << k_seed_types_yaml;
        }
    }
    std::fprintf(stdout,
        "[graph_explorer] migrated legacy layout to projects/%s/\n",
        k_default_project);
    return true;
}

// ----------------------------------------------------------------------------
// Settings (graph_explorer.ini)
// ----------------------------------------------------------------------------

static std::string trim(const std::string& s) {
    size_t a = 0, b = s.size();
    while (a < b && std::isspace((unsigned char)s[a])) ++a;
    while (b > a && std::isspace((unsigned char)s[b - 1])) --b;
    return s.substr(a, b - a);
}

static std::vector<std::string> split_csv(const std::string& s) {
    std::vector<std::string> out;
    std::string cur;
    for (char c : s) {
        if (c == ',') {
            std::string t = trim(cur);
            if (!t.empty()) out.push_back(t);
            cur.clear();
        } else {
            cur.push_back(c);
        }
    }
    std::string t = trim(cur);
    if (!t.empty()) out.push_back(t);
    return out;
}

bool project_settings_load(ProjectSettings* out) {
    if (!out) return false;
    *out = {};
    std::ifstream in(settings_path());
    if (!in) return true; // no es error: archivo aun no existe
    std::string line;
    while (std::getline(in, line)) {
        std::string t = trim(line);
        if (t.empty() || t[0] == '#') continue;
        size_t eq = t.find('=');
        if (eq == std::string::npos) continue;
        std::string key = trim(t.substr(0, eq));
        std::string val = trim(t.substr(eq + 1));
        if (key == "last_active") out->last_active = val;
        else if (key == "recent")  out->recent     = split_csv(val);
    }
    return true;
}

bool project_settings_save(const ProjectSettings& s) {
    std::ofstream out(settings_path());
    if (!out) return false;
    out << "# graph_explorer.ini — autogenerado, editable\n";
    out << "last_active = " << s.last_active << "\n";
    out << "recent      = ";
    for (size_t i = 0; i < s.recent.size(); ++i) {
        if (i) out << ",";
        out << s.recent[i];
    }
    out << "\n";
    return true;
}

void project_settings_touch(const char* slug) {
    if (!slug || !*slug) return;
    ProjectSettings s;
    project_settings_load(&s);
    s.last_active = slug;
    // Push al frente, dedup, max 5
    std::vector<std::string> n;
    n.reserve(6);
    n.emplace_back(slug);
    for (const auto& r : s.recent) {
        if (r == slug) continue;
        n.push_back(r);
        if (n.size() >= 5) break;
    }
    s.recent = std::move(n);
    project_settings_save(s);
}

// ----------------------------------------------------------------------------
// Reveal in explorer
// ----------------------------------------------------------------------------

void project_reveal_in_explorer(const char* slug) {
    if (!slug || !*slug) return;
    ProjectPaths p = project_paths(slug);
    std::error_code ec;
    if (!fs::exists(p.root_dir, ec)) return;
    std::string abs = fs::absolute(p.root_dir, ec).string();

#if defined(_WIN32)
    // Reemplaza '/' por '\\' para que ShellExecute reconozca el path.
    for (char& c : abs) if (c == '/') c = '\\';
    ShellExecuteA(nullptr, "open", abs.c_str(), nullptr, nullptr, SW_SHOWDEFAULT);
#else
    std::string cmd = "xdg-open '";
    cmd += abs;
    cmd += "' >/dev/null 2>&1 &";
    int rc = std::system(cmd.c_str());
    (void)rc;
#endif
}

} // namespace ge
