const fs = require("fs");
const path = require("path");
const { writeJsonAtomicSync } = require("../../../src/services/safe-files");
const { spawnSync } = require("child_process");
const { resolveData } = require("./paths");

const DEFAULT_REPOSITORY = "https://git.rolfsvaag.no/Rolfsvaag_Datateknikk/Lumi";
const CACHE_TTL_MS = 5 * 60 * 1000;

class ToolRepoClient {
  constructor(options = {}) {
    this.settings = options.settings;
    this.fetch = options.fetch || global.fetch;
    this.repoRoot = options.repoRoot || path.resolve(__dirname, "..", "..", "..");
    this.cacheFile = options.cacheFile || resolveData("tools", "remote-metadata.json");
    this.now = options.now || Date.now;
    this.runGit = options.runGit || ((args) => spawnSync("git", args, {
      cwd: this.repoRoot,
      encoding: "utf8",
      timeout: 10000
    }));
  }

  source() {
    const configured = String(this.settings?.getSetting?.("git_remote", "origin") || "origin").trim();
    const branch = String(this.settings?.getSetting?.("git_branch", "main") || "main").trim() || "main";
    let repository = looksLikeRepositoryUrl(configured) ? configured : "";
    if (!repository) {
      const result = this.runGit(["remote", "get-url", configured]);
      if (result?.status === 0) repository = String(result.stdout || "").trim();
    }
    try {
      repository = normalizeRepositoryUrl(repository || DEFAULT_REPOSITORY);
    } catch {
      repository = normalizeRepositoryUrl(DEFAULT_REPOSITORY);
    }
    return { repository, branch, ...parseRepositoryUrl(repository) };
  }

  async discover({ force = false } = {}) {
    const source = this.source();
    const cached = this.readCache();
    const cacheMatches = cached?.repository === source.repository && cached?.branch === source.branch;
    if (!force && cacheMatches && this.now() - Number(cached.checked_at_ms) < CACHE_TTL_MS) {
      return { ...cached, cached: true, stale: false };
    }
    try {
      const rows = await this.requestJson(this.contentsUrl(source, "plugins"));
      const directories = (Array.isArray(rows) ? rows : [])
        .filter((row) => row.type === "dir" && /^lumi_ai_[a-z0-9_-]+$/i.test(row.name));
      const tools = await Promise.all(directories.map(async (row) => {
        try {
          const metadata = await this.readRemoteJson(row.name, "tool_info.json", source);
          validateRemoteMetadata(metadata, row.name);
          return { ...metadata, repository_path: metadata.repository_path || `plugins/${row.name}` };
        } catch (error) {
          return {
            tool_id: row.name,
            display_name: row.name,
            version: "0.0.0",
            description: "",
            scope: "unavailable",
            permissions: [],
            capabilities: [],
            limitations: [],
            remote_invalid: true,
            remote_error: error.message,
            repository_path: `plugins/${row.name}`
          };
        }
      }));
      const payload = {
        repository: source.repository,
        branch: source.branch,
        checked_at: new Date(this.now()).toISOString(),
        checked_at_ms: this.now(),
        tools
      };
      this.writeCache(payload);
      return { ...payload, cached: false, stale: false };
    } catch (error) {
      if (cacheMatches) {
        return { ...cached, cached: true, stale: true, error: error.message };
      }
      return {
        repository: source.repository,
        branch: source.branch,
        checked_at: null,
        checked_at_ms: 0,
        tools: [],
        cached: false,
        stale: true,
        error: error.message
      };
    }
  }

  async readReadme(toolId) {
    return this.readRemoteText(toolId, "readme.md", this.source());
  }

  async downloadTool(toolId, destination) {
    assertToolId(toolId);
    fs.mkdirSync(destination, { recursive: true });
    await this.downloadDirectory(`plugins/${toolId}`, destination, this.source());
    return destination;
  }

  async downloadDirectory(repositoryPath, destination, source) {
    const entries = await this.requestJson(this.contentsUrl(source, repositoryPath));
    if (!Array.isArray(entries)) throw new Error(`Remote path ${repositoryPath} is not a directory.`);
    for (const entry of entries) {
      const target = safeChild(destination, entry.name);
      if (entry.type === "dir") {
        fs.mkdirSync(target, { recursive: true });
        await this.downloadDirectory(entry.path, target, source);
      } else if (entry.type === "file") {
        const file = await this.requestJson(this.contentsUrl(source, entry.path));
        const content = await this.readFileContent(file, source);
        fs.mkdirSync(path.dirname(target), { recursive: true });
        fs.writeFileSync(target, content);
      }
    }
  }

  async readRemoteJson(toolId, filename, source = this.source()) {
    return JSON.parse(await this.readRemoteText(toolId, filename, source));
  }

  async readRemoteText(toolId, filename, source = this.source()) {
    assertToolId(toolId);
    const file = await this.requestJson(this.contentsUrl(source, `plugins/${toolId}/${filename}`));
    return (await this.readFileContent(file, source)).toString("utf8");
  }

  async readFileContent(file, source) {
    if (typeof file?.content === "string") return decodeContent(file);
    if (!file?.download_url) throw new Error("Remote file content is unavailable.");
    const download = new URL(file.download_url);
    if (download.origin !== new URL(source.repository).origin) {
      throw new Error("Remote file download URL is outside the configured repository host.");
    }
    return this.requestBuffer(download.href);
  }

  contentsUrl(source, repositoryPath) {
    const encodedPath = repositoryPath.split("/").map(encodeURIComponent).join("/");
    return `${source.api_base}/contents/${encodedPath}?ref=${encodeURIComponent(source.branch)}`;
  }

  async requestJson(url) {
    if (typeof this.fetch !== "function") throw new Error("Remote repository access is unavailable.");
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), 10000);
    timer.unref?.();
    try {
      const response = await this.fetch(url, {
        headers: { Accept: "application/json", "User-Agent": "Lumi-AI-Tool-Manager" },
        signal: controller.signal
      });
      if (!response.ok) throw new Error(`Repository request failed (${response.status}).`);
      return response.json();
    } finally {
      clearTimeout(timer);
    }
  }

  async requestBuffer(url) {
    if (typeof this.fetch !== "function") throw new Error("Remote repository access is unavailable.");
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), 10000);
    timer.unref?.();
    try {
      const response = await this.fetch(url, {
        headers: { Accept: "application/octet-stream", "User-Agent": "Lumi-AI-Tool-Manager" },
        signal: controller.signal
      });
      if (!response.ok) throw new Error(`Repository download failed (${response.status}).`);
      return Buffer.from(await response.arrayBuffer());
    } finally {
      clearTimeout(timer);
    }
  }

  readCache() {
    try {
      const value = JSON.parse(fs.readFileSync(this.cacheFile, "utf8"));
      return Array.isArray(value.tools) ? value : null;
    } catch {
      return null;
    }
  }

  writeCache(value) {
    writeJsonAtomicSync(this.cacheFile, value);
  }
}

function normalizeRepositoryUrl(value) {
  let url = String(value || "").trim().replace(/\.git\/?$/i, "").replace(/\/+$/, "");
  const ssh = url.match(/^git@([^:]+):(.+)$/);
  if (ssh) url = `https://${ssh[1]}/${ssh[2]}`;
  if (url.startsWith("ssh://git@")) {
    const parsed = new URL(url);
    url = `https://${parsed.hostname}${parsed.pathname}`;
  }
  const parsed = new URL(url);
  if (!["http:", "https:"].includes(parsed.protocol)) throw new Error("Configured Git repository must use HTTP(S) or a convertible SSH URL.");
  parsed.username = "";
  parsed.password = "";
  return parsed.href.replace(/\/$/, "").replace(/\.git$/i, "");
}

function parseRepositoryUrl(repository) {
  const parsed = new URL(repository);
  const parts = parsed.pathname.split("/").filter(Boolean);
  if (parts.length < 2) throw new Error("Configured repository URL must include owner and repository.");
  const owner = parts.at(-2);
  const repo = parts.at(-1);
  return {
    owner,
    repo,
    api_base: `${parsed.origin}/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`
  };
}

function looksLikeRepositoryUrl(value) {
  return /^(?:https?:\/\/|ssh:\/\/|git@)/i.test(value);
}

function decodeContent(file) {
  if (!file || file.type !== "file" || typeof file.content !== "string") {
    throw new Error("Remote file content is unavailable.");
  }
  if (file.encoding && file.encoding !== "base64") throw new Error(`Unsupported repository encoding: ${file.encoding}.`);
  return Buffer.from(file.content.replace(/\s+/g, ""), "base64");
}

function assertToolId(toolId) {
  if (!/^lumi_ai_[a-z0-9_-]+$/i.test(String(toolId || ""))) throw new Error("Invalid Lumi AI tool plugin ID.");
}

function safeChild(parent, name) {
  if (!name || name !== path.basename(name)) throw new Error("Remote tool path is unsafe.");
  const target = path.resolve(parent, name);
  if (!target.startsWith(`${path.resolve(parent)}${path.sep}`)) throw new Error("Remote tool path escapes staging.");
  return target;
}

function validateRemoteMetadata(metadata, folderName) {
  for (const field of [
    "tool_id", "display_name", "version", "description", "scope", "permissions", "capabilities", "limitations"
  ]) {
    if (metadata?.[field] == null || metadata[field] === "") throw new Error(`tool_info.json is missing ${field}.`);
  }
  if (metadata.tool_id !== folderName) throw new Error("tool_id must match the remote folder name.");
}

module.exports = {
  CACHE_TTL_MS,
  DEFAULT_REPOSITORY,
  ToolRepoClient,
  assertToolId,
  decodeContent,
  looksLikeRepositoryUrl,
  normalizeRepositoryUrl,
  parseRepositoryUrl,
  validateRemoteMetadata
};
