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

const repoRoot = path.join(__dirname, "..", "..");
const dataDir = path.join(repoRoot, "data");
const updateCacheDir = path.join(dataDir, "update-cache");
const managedRepoDir = path.join(updateCacheDir, "repo");
const updateStatePath = path.join(dataDir, "update-state.json");
const DEFAULT_REPOSITORY = "https://git.rolfsvaag.no/Rolfsvaag_Datateknikk/Lumi";
const PRESERVED_PATHS = [
  "data",
  "config",
  "storage",
  "uploads",
  "logs",
  "database",
  "databases",
  "plugins",
  "knowledge/community",
  "knowledge/corrections",
  ".env",
  ".env.local",
  ".env.production",
  ".secrets",
  "codex-guidelines"
];

function runGit(args, options = {}) {
  const result = spawnSync("git", args, {
    cwd: options.cwd || repoRoot,
    encoding: "utf8",
    timeout: options.timeout || 120000
  });
  if (result.status !== 0) {
    throw new Error((result.stderr || result.stdout || "Git command failed.").trim());
  }
  return result.stdout.trim();
}

function tryGit(args, options = {}, fallback = "") {
  try {
    return runGit(args, options);
  } catch {
    return fallback;
  }
}

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

function normalizeRepositoryTarget(value) {
  const target = String(value || DEFAULT_REPOSITORY).trim() || DEFAULT_REPOSITORY;
  if (isRepositoryUrl(target)) {
    return target.replace(/\.git\/?$/i, "").replace(/\/+$/, "");
  }
  return target;
}

function isGitRepository(directory = repoRoot) {
  return tryGit(["rev-parse", "--is-inside-work-tree"], { cwd: directory }, "") === "true";
}

function readUpdateState() {
  try {
    const state = JSON.parse(fs.readFileSync(updateStatePath, "utf8"));
    return state && typeof state === "object" ? state : {};
  } catch {
    return {};
  }
}

function writeUpdateState(values = {}) {
  fs.mkdirSync(dataDir, { recursive: true });
  const current = readUpdateState();
  const next = {
    schema_version: 1,
    preserve_paths: PRESERVED_PATHS,
    managed_repo_path: managedRepoDir,
    updated_at: new Date().toISOString(),
    ...current,
    ...values
  };
  writeJsonAtomicSync(updateStatePath, next);
  return next;
}

function resolveRepositoryRemote(value) {
  const target = normalizeRepositoryTarget(value);
  if (isRepositoryUrl(target)) {
    return target;
  }

  const state = readUpdateState();
  if (isRepositoryUrl(state.remote)) {
    return normalizeRepositoryTarget(state.remote);
  }

  if (isGitRepository(managedRepoDir)) {
    const cached = tryGit(["remote", "get-url", target], { cwd: managedRepoDir }, "");
    if (cached) return normalizeRepositoryTarget(cached);
    const origin = tryGit(["remote", "get-url", "origin"], { cwd: managedRepoDir }, "");
    if (origin) return normalizeRepositoryTarget(origin);
  }

  if (isGitRepository(repoRoot)) {
    const live = tryGit(["remote", "get-url", target], { cwd: repoRoot }, "");
    if (live) return normalizeRepositoryTarget(live);
  }

  if (target === "origin") {
    return DEFAULT_REPOSITORY;
  }

  throw new Error("Configure Git remote / repository target as a repository URL for ZIP-origin installs.");
}

function resolveSourceBranch(remote, requested = "stable") {
  const repository = resolveRepositoryRemote(remote);
  if (requested === "experimental") {
    const refs = runGit(["ls-remote", "--heads", repository, "experimental-*"], { timeout: 60000 });
    const branches = refs
      .split(/\r?\n/)
      .map((line) => line.trim().split(/\s+/)[1] || "")
      .map((ref) => ref.replace(/^refs\/heads\//, ""))
      .filter(Boolean)
      .sort((a, b) => b.localeCompare(a));
    if (branches[0]) return branches[0];
    return "experimental";
  }
  if (requested && requested !== "stable" && requested !== "main") {
    return String(requested).replace(/^origin\//, "");
  }
  return "main";
}

function migrateZipEraLayout(remote) {
  fs.mkdirSync(updateCacheDir, { recursive: true });
  const state = readUpdateState();
  if (state.migrated_zip_layout_at) {
    return state;
  }
  return writeUpdateState({
    migrated_zip_layout_at: new Date().toISOString(),
    migration: "zip-era-layout",
    remote: resolveRepositoryRemote(remote),
    live_install_git_repository: isGitRepository(repoRoot)
  });
}

function resetManagedRepo(repository) {
  fs.rmSync(managedRepoDir, { recursive: true, force: true });
  fs.mkdirSync(updateCacheDir, { recursive: true });
  runGit(["clone", repository, managedRepoDir], {
    cwd: updateCacheDir,
    timeout: 300000
  });
}

function normalizeRepositoryRef(value) {
  const reference = String(value || "main").trim();
  if (!reference || reference.length > 180 || reference.startsWith("-") || reference.includes("..") || reference.includes("@{")) {
    throw new Error("Invalid repository version reference.");
  }
  if (!/^(?:refs\/(?:heads|tags)\/)?[A-Za-z0-9][A-Za-z0-9._/-]*$/.test(reference)) {
    throw new Error("Invalid repository version reference.");
  }
  return reference.replace(/^origin\//, "");
}

function normalizeDeploymentBranch(value) {
  const reference = normalizeRepositoryRef(value).replace(/^refs\/heads\//, "");
  if (reference === "main") return reference;
  if (/^experimental-[A-Za-z0-9][A-Za-z0-9._\/-]*$/.test(reference)) return reference;
  throw new Error("Choose main or an experimental-* branch published by the configured repository.");
}

function parseDeploymentBranches(output) {
  const branches = new Map();
  for (const line of String(output || "").split(/\r?\n/)) {
    const [commit = "", ref = ""] = line.trim().split(/\s+/);
    if (!/^[a-f0-9]{40,64}$/i.test(commit) || !ref.startsWith("refs/heads/")) continue;
    const name = ref.slice("refs/heads/".length);
    try {
      const normalized = normalizeDeploymentBranch(name);
      branches.set(normalized, {
        name: normalized,
        commit,
        channel: normalized === "main" ? "stable" : "experimental"
      });
    } catch {
      // Only stable main and explicitly named experimental branches are deployable.
    }
  }
  return Array.from(branches.values()).sort((left, right) => {
    if (left.name === "main") return -1;
    if (right.name === "main") return 1;
    return right.name.localeCompare(left.name);
  });
}

function listDeploymentBranches(remote) {
  const repository = resolveRepositoryRemote(remote);
  const output = runGit([
    "ls-remote",
    "--heads",
    repository,
    "refs/heads/main",
    "refs/heads/experimental-*"
  ], { timeout: 60000 });
  return {
    repository,
    branches: parseDeploymentBranches(output)
  };
}

function managedTargetForRef(reference) {
  if (reference.startsWith("refs/tags/")) return reference;
  if (reference.startsWith("refs/heads/")) return `refs/remotes/origin/${reference.slice("refs/heads/".length)}`;
  return `refs/remotes/origin/${reference}`;
}

function ensureManagedRepo(remote, branch) {
  const repository = resolveRepositoryRemote(remote);
  const reference = normalizeRepositoryRef(branch);
  migrateZipEraLayout(repository);
  fs.mkdirSync(updateCacheDir, { recursive: true });

  if (!isGitRepository(managedRepoDir)) {
    resetManagedRepo(repository);
  } else {
    const currentRemote = normalizeRepositoryTarget(
      tryGit(["remote", "get-url", "origin"], { cwd: managedRepoDir }, "")
    );
    if (currentRemote && currentRemote !== repository) {
      runGit(["remote", "set-url", "origin", repository], { cwd: managedRepoDir });
    } else if (!currentRemote) {
      resetManagedRepo(repository);
    }
  }

  runGit(["fetch", "--prune", "--tags", "origin"], {
    cwd: managedRepoDir,
    timeout: 300000
  });
  const target = managedTargetForRef(reference);
  runGit(["rev-parse", "--verify", `${target}^{commit}`], {
    cwd: managedRepoDir,
    timeout: 60000
  });
  runGit(["checkout", "--detach", target], {
    cwd: managedRepoDir,
    timeout: 120000
  });
  runGit(["reset", "--hard", target], {
    cwd: managedRepoDir,
    timeout: 120000
  });
  runGit(["clean", "-fdx"], {
    cwd: managedRepoDir,
    timeout: 120000
  });

  writeUpdateState({
    remote: repository,
    branch: reference,
    commit: runGit(["rev-parse", "HEAD"], { cwd: managedRepoDir }),
    managed_repo_path: managedRepoDir,
    last_fetch_at: new Date().toISOString(),
    live_install_git_repository: isGitRepository(repoRoot)
  });
  return {
    repository,
    branch: reference,
    commit: runGit(["rev-parse", "HEAD"], { cwd: managedRepoDir }),
    path: managedRepoDir
  };
}

function createMetadataReader(remote, branch) {
  const managed = ensureManagedRepo(remote, branch);
  return {
    repository: managed.repository,
    branch: managed.branch,
    root: managed.path,
    readFile(filePath) {
      const target = path.resolve(managed.path, filePath);
      if (!target.startsWith(managed.path + path.sep) && target !== managed.path) {
        return null;
      }
      try {
        return fs.readFileSync(target, "utf8");
      } catch {
        return null;
      }
    },
    listPluginDirs() {
      const pluginsPath = path.join(managed.path, "plugins");
      try {
        return fs.readdirSync(pluginsPath, { withFileTypes: true })
          .filter((entry) => entry.isDirectory())
          .map((entry) => entry.name);
      } catch {
        return [];
      }
    },
    cleanup() {}
  };
}

module.exports = {
  DEFAULT_REPOSITORY,
  PRESERVED_PATHS,
  updateCacheDir,
  managedRepoDir,
  updateStatePath,
  runGit,
  isRepositoryUrl,
  isGitRepository,
  normalizeRepositoryTarget,
  normalizeRepositoryRef,
  normalizeDeploymentBranch,
  parseDeploymentBranches,
  listDeploymentBranches,
  resolveRepositoryRemote,
  resolveSourceBranch,
  migrateZipEraLayout,
  ensureManagedRepo,
  createMetadataReader,
  readUpdateState,
  writeUpdateState
};
