import fs from "node:fs";
import path from "node:path";

import { TASK_PATH_PREFIXES } from "../core/constants.mjs";
import { run, runJson } from "../core/exec.mjs";
import { log } from "../core/logging.mjs";
import { unique } from "../core/utils.mjs";

const RELATED_REPO_PREFIX = "related";

export const buildRelatedPath = (repoSlug, filePath) => {
  if (null == repoSlug || null == filePath) {
    return filePath || "";
  }
  return `${RELATED_REPO_PREFIX}/${repoSlug}/${filePath}`.replace(/\/+/g, "/");
};

export const stripRelatedPrefix = (filePath) => {
  if (null == filePath) {
    return filePath;
  }
  const match = filePath.match(/^related\/[^/]+\/[^/]+\/(.+)$/);
  return match ? match[1] : filePath;
};

export const normalizeTaskPath = (filePath) => stripRelatedPrefix(filePath);

export const resolveMode = ({ mode, baseRef, prNumber }) => {
  if (null !== mode && "auto" !== mode) {
    return mode;
  }
  if (null !== prNumber) {
    return "pr-compare";
  }
  if (null !== baseRef) {
    return "branch-compare";
  }
  return "working-tree";
};

export const resolveRepoArg = (value, repoRoot) => {
  if (null === value) {
    return null;
  }
  const resolvedPath =
    true === path.isAbsolute(value) || null == repoRoot
      ? value
      : path.join(repoRoot, value);
  if (true === fs.existsSync(resolvedPath)) {
    const remoteUrl = run("git", [
      "-C",
      resolvedPath,
      "remote",
      "get-url",
      "origin",
    ]);
    const match = remoteUrl.match(/github\.com[:/](.+?)(?:\.git)?$/);
    if (null !== match) {
      return match[1];
    }
  }
  return value;
};

export const resolveLocalRepoPath = (value, repoRoot) => {
  if (null === value) {
    return null;
  }
  const resolvedPath =
    true === path.isAbsolute(value) || null == repoRoot
      ? value
      : path.join(repoRoot, value);
  if (true === fs.existsSync(resolvedPath)) {
    return resolvedPath;
  }
  return null;
};

export const parseRepoSlug = (repoSlug) => {
  if (null == repoSlug) {
    return { owner: null, repo: null };
  }
  const parts = repoSlug.split("/");
  if (parts.length < 2) {
    return { owner: null, repo: null };
  }
  return { owner: parts[0], repo: parts.slice(1).join("/") };
};

export const fetchPrFiles = ({ prNumber, repoSlug }) => {
  if (null == prNumber || null == repoSlug) {
    return [];
  }
  const { owner, repo } = parseRepoSlug(repoSlug);
  if (null == owner || null == repo) {
    return [];
  }
  const response = runJson("gh", [
    "api",
    `repos/${owner}/${repo}/pulls/${prNumber}/files`,
    "--paginate",
    "-H",
    "Accept: application/vnd.github+json",
  ]);
  if (false === Array.isArray(response)) {
    return [];
  }
  return response;
};

export const fetchCompareDiff = ({ repoSlug, baseSha, headSha }) => {
  if (null == repoSlug || null == baseSha || null == headSha) {
    return "";
  }
  const { owner, repo } = parseRepoSlug(repoSlug);
  if (null == owner || null == repo) {
    return "";
  }
  return run("gh", [
    "api",
    `repos/${owner}/${repo}/compare/${baseSha}...${headSha}`,
    "-H",
    "Accept: application/vnd.github.v3.diff",
  ]);
};

export const fetchPrCommits = ({ prNumber, repoSlug }) => {
  if (null == prNumber || null == repoSlug) {
    return [];
  }
  const { owner, repo } = parseRepoSlug(repoSlug);
  if (null == owner || null == repo) {
    return [];
  }
  const response = runJson("gh", [
    "api",
    `repos/${owner}/${repo}/pulls/${prNumber}/commits`,
    "--paginate",
    "-H",
    "Accept: application/vnd.github+json",
  ]);
  return Array.isArray(response) ? response : [];
};

export const fetchReviewThreads = ({ prNumber, repoSlug }) => {
  if (null == prNumber || null == repoSlug) {
    return [];
  }
  const { owner, repo } = parseRepoSlug(repoSlug);
  if (null == owner || null == repo) {
    return [];
  }
  const query = [
    "query($owner:String!,$repo:String!,$number:Int!,$after:String) {",
    "  repository(owner:$owner, name:$repo) {",
    "    pullRequest(number:$number) {",
    "      reviewThreads(first:50, after:$after) {",
    "        pageInfo { hasNextPage endCursor }",
    "        nodes {",
    "          id",
    "          isResolved",
    "          resolvedAt",
    "          resolvedBy { login }",
    "          comments(first:100) {",
    "            nodes {",
    "              id",
    "              databaseId",
    "              body",
    "              createdAt",
    "              author { login }",
    "              path",
    "              position",
    "              originalLine",
    "              line",
    "              diffHunk",
    "              url",
    "            }",
    "          }",
    "        }",
    "      }",
    "    }",
    "  }",
    "}",
  ].join("\n");
  const threads = [];
  let cursor = null;
  let hasNextPage = true;
  while (true === hasNextPage) {
    const args = [
      "api",
      "graphql",
      "-f",
      `query=${query}`,
      "-f",
      `owner=${owner}`,
      "-f",
      `repo=${repo}`,
      "-F",
      `number=${Number(prNumber)}`,
    ];
    if (cursor) {
      args.push("-f", `after=${cursor}`);
    }
    const response = runJson("gh", args);
    const payload = response?.data?.repository?.pullRequest?.reviewThreads;
    const nodes = Array.isArray(payload?.nodes) ? payload.nodes : [];
    threads.push(...nodes);
    hasNextPage = true === payload?.pageInfo?.hasNextPage;
    cursor = payload?.pageInfo?.endCursor || null;
    if (!hasNextPage) {
      break;
    }
  }
  return threads;
};

export const isTaskFile = (filePath) => {
  const normalized = normalizeTaskPath(filePath);
  return (
    null !== normalized &&
    TASK_PATH_PREFIXES.some((prefix) => normalized.startsWith(prefix))
  );
};

export const splitChangedFiles = (files) => {
  const taskFiles = [];
  const codeFiles = [];
  files.forEach((filePath) => {
    if (isTaskFile(filePath)) {
      taskFiles.push(filePath);
    } else {
      codeFiles.push(filePath);
    }
  });
  return { taskFiles, codeFiles };
};

export const extractIssueFromTaskPath = (filePath) => {
  const normalized = normalizeTaskPath(filePath);
  if (null === normalized) {
    return null;
  }
  const match = normalized.match(
    /(?:includes\/builder-5\/)?\.?et\/tasks\/\d+\/(\d+)\//
  );
  if (match) {
    return match[1];
  }
  const cursorMatch = normalized.match(/\.cursor\/tasks\/(\d+)\//);
  return cursorMatch ? cursorMatch[1] : null;
};

export const getTaskRoot = (repoRoot) => {
  const builderTaskRoot = path.join(
    repoRoot,
    "includes/builder-5/et/tasks"
  );
  if (true === fs.existsSync(builderTaskRoot)) {
    return builderTaskRoot;
  }
  const builderHiddenTaskRoot = path.join(
    repoRoot,
    "includes/builder-5/.et/tasks"
  );
  if (true === fs.existsSync(builderHiddenTaskRoot)) {
    return builderHiddenTaskRoot;
  }
  const cursorTaskRoot = path.join(repoRoot, ".cursor/tasks");
  if (true === fs.existsSync(cursorTaskRoot)) {
    return cursorTaskRoot;
  }
  return path.join(repoRoot, "et/tasks");
};

export const resolveTaskRoot = (repoRoot, taskFiles = []) => {
  if (taskFiles.some((filePath) => filePath.startsWith(".cursor/tasks/"))) {
    return path.join(repoRoot, ".cursor/tasks");
  }
  if (
    taskFiles.some((filePath) =>
      filePath.startsWith("includes/builder-5/.et/tasks/")
    )
  ) {
    return path.join(repoRoot, "includes/builder-5/.et/tasks");
  }
  if (
    taskFiles.some((filePath) =>
      filePath.startsWith("includes/builder-5/et/tasks/")
    )
  ) {
    return path.join(repoRoot, "includes/builder-5/et/tasks");
  }
  if (taskFiles.some((filePath) => filePath.startsWith("et/tasks/"))) {
    return path.join(repoRoot, "et/tasks");
  }
  return getTaskRoot(repoRoot);
};

export const getImplementationPlanPath = (
  repoRoot,
  issueNumber,
  taskFiles = []
) => {
  if (null == repoRoot || null == issueNumber) {
    return null;
  }
  const taskRoot = resolveTaskRoot(repoRoot, taskFiles);
  if (taskRoot.endsWith(path.join(".cursor", "tasks"))) {
    return path.join(taskRoot, String(issueNumber), "implementation-plan.md");
  }
  return path.join(
    taskRoot,
    String(issueNumber),
    "implementation-plan.md"
  );
};

export const getPlanExcerpt = (planPath) => {
  if (null === planPath || false === fs.existsSync(planPath)) {
    return null;
  }
  const contents = fs.readFileSync(planPath, "utf8");
  const sections = [];
  const lines = contents.split("\n");
  const captureSection = (heading) => {
    const index = lines.findIndex((line) => line.trim() === heading);
    if (-1 === index) {
      return;
    }
    const collected = [];
    for (let i = index; i < lines.length; i += 1) {
      const line = lines[i];
      if (index !== i && true === line.startsWith("## ")) {
        break;
      }
      collected.push(line);
    }
    sections.push(collected.join("\n").trim());
  };
  captureSection("## Problem Analysis");
  captureSection("## Solution Approach");
  const excerpt = sections.filter(Boolean).join("\n\n").trim();
  if ("" === excerpt) {
    return null;
  }
  return excerpt.slice(0, 4000);
};

export const buildTaskContext = (repoRoot, taskFiles) => {
  const issueNumbers = unique(
    taskFiles.map(extractIssueFromTaskPath).filter(Boolean)
  );
  const primaryIssueNumber = issueNumbers[0] || null;
  const planPath = getImplementationPlanPath(
    repoRoot,
    primaryIssueNumber,
    taskFiles
  );
  const planExcerpt = getPlanExcerpt(planPath);
  return {
    issue_numbers: issueNumbers,
    primary_issue_number: primaryIssueNumber,
    implementation_plan_path:
      null !== planPath && true === fs.existsSync(planPath) ? planPath : null,
    implementation_plan_excerpt: planExcerpt,
  };
};

export const applyPrBodyTaskFallback = (taskContext, prBody) => {
  const body = null == prBody ? "" : String(prBody).trim();
  if ("" === body) {
    return taskContext;
  }
  const context = taskContext || {};
  if (0 < (context.issue_numbers || []).length) {
    return context;
  }
  const excerpt = body.slice(0, 4000);
  return {
    ...context,
    task_context_source: "pr_body",
    pr_description: excerpt,
  };
};

export const filterPatchByPredicate = (patch, shouldInclude) => {
  if (null === patch || "" === patch) {
    return patch;
  }
  const lines = patch.split("\n");
  const output = [];
  let current = [];
  let includeBlock = true;
  const flush = () => {
    if (includeBlock && current.length) {
      output.push(...current);
    }
    current = [];
  };
  lines.forEach((line) => {
    if (line.startsWith("diff --git ")) {
      flush();
      current.push(line);
      const match = line.match(/^diff --git a\/(.+?) b\/(.+)$/);
      const filePath = match ? match[2] : null;
      includeBlock = shouldInclude(filePath);
      return;
    }
    current.push(line);
  });
  flush();
  return output.join("\n").trimEnd();
};

export const countPatchLines = (patch) => {
  if (null === patch || "" === patch) {
    return 0;
  }
  return patch
    .split("\n")
    .filter((line) => {
      if ("" === line) {
        return false;
      }
      if (line.startsWith("+++")) {
        return false;
      }
      if (line.startsWith("---")) {
        return false;
      }
      if (line.startsWith("@@")) {
        return false;
      }
      if (line.startsWith("diff --git")) {
        return false;
      }
      return line.startsWith("+") || line.startsWith("-");
    }).length;
};

export const parseNumstat = (output) =>
  output
    .split("\n")
    .map((line) => line.trim())
    .filter(Boolean)
    .map((line) => {
      const [additionsRaw, deletionsRaw, ...pathParts] = line.split(/\s+/);
      const filePath = pathParts.join(" ");
      const additions = "-" === additionsRaw ? null : Number(additionsRaw);
      const deletions = "-" === deletionsRaw ? null : Number(deletionsRaw);
      return {
        path: filePath,
        additions,
        deletions,
      };
    })
    .filter((entry) => entry.path);

export const parseNameStatus = (output) =>
  output
    .split("\n")
    .map((line) => line.trim())
    .filter(Boolean)
    .map((line) => {
      const parts = line.split(/\s+/);
      const status = parts[0];
      if (status.startsWith("R") || status.startsWith("C")) {
        return {
          status,
          path: parts[2] || parts[1],
          oldPath: parts[1] || null,
        };
      }
      return {
        status,
        path: parts[1] || parts[0],
      };
    })
    .filter((entry) => entry.path);

export const mergeNumstats = (entries) => {
  const merged = new Map();
  entries.forEach((entry) => {
    const existing = merged.get(entry.path);
    if (!existing) {
      merged.set(entry.path, { ...entry });
      return;
    }
    merged.set(entry.path, {
      path: entry.path,
      additions:
        null == existing.additions || null == entry.additions
          ? null
          : existing.additions + entry.additions,
      deletions:
        null == existing.deletions || null == entry.deletions
          ? null
          : existing.deletions + entry.deletions,
    });
  });
  return Array.from(merged.values());
};

export const getNumstatForMode = ({ mode, baseRef, headRef }) => {
  if ("working-tree" === mode) {
    const unstaged = parseNumstat(run("git", ["diff", "--numstat"]));
    const staged = parseNumstat(run("git", ["diff", "--cached", "--numstat"]));
    return mergeNumstats([...unstaged, ...staged]);
  }
  if ("branch-compare" === mode) {
    return parseNumstat(
      run("git", ["diff", "--numstat", `${baseRef}...${headRef}`])
    );
  }
  return [];
};

export const getNameStatusForMode = ({ mode, baseRef, headRef }) => {
  if ("working-tree" === mode) {
    const unstaged = parseNameStatus(run("git", ["diff", "--name-status"]));
    const staged = parseNameStatus(
      run("git", ["diff", "--cached", "--name-status"])
    );
    return [...unstaged, ...staged];
  }
  if ("branch-compare" === mode) {
    return parseNameStatus(
      run("git", ["diff", "--name-status", `${baseRef}...${headRef}`])
    );
  }
  return [];
};

export const getFilePatch = ({
  mode,
  baseRef,
  headRef,
  prNumber,
  repoSlug,
  filePath,
  contextLines,
  filePatch,
}) => {
  if (null == filePath || "" === filePath) {
    return "";
  }
  if ("working-tree" === mode) {
    const unstaged = run("git", [
      "diff",
      "--patch",
      `--unified=${contextLines}`,
      "--",
      filePath,
    ]);
    const staged = run("git", [
      "diff",
      "--cached",
      "--patch",
      `--unified=${contextLines}`,
      "--",
      filePath,
    ]);
    return [unstaged, staged].filter((patch) => patch && patch.trim()).join("\n");
  }
  if ("branch-compare" === mode) {
    return run("git", [
      "diff",
      "--patch",
      `--unified=${contextLines}`,
      `${baseRef}...${headRef}`,
      "--",
      filePath,
    ]);
  }
  if ("pr-compare" === mode) {
    return filePatch || "";
  }
  return "";
};

export const splitPatchIntoChunks = (patch, maxLines = 220) => {
  if (null === patch || "" === patch) {
    return [];
  }
  const lines = patch.split("\n");
  const header = [];
  let index = 0;
  while (index < lines.length && false === lines[index].startsWith("@@")) {
    header.push(lines[index]);
    index += 1;
  }
  if (index >= lines.length) {
    return [patch.trimEnd()];
  }
  const chunks = [];
  let current = [...header];
  let currentLines = header.length;
  let currentHasHunk = false;
  for (; index < lines.length; index += 1) {
    const line = lines[index];
    if (
      true === line.startsWith("@@") &&
      true === currentHasHunk &&
      currentLines >= maxLines
    ) {
      chunks.push(current.join("\n").trimEnd());
      current = [...header];
      currentLines = header.length;
      currentHasHunk = false;
    }
    current.push(line);
    currentLines += 1;
    if (true === line.startsWith("@@")) {
      currentHasHunk = true;
    }
  }
  if (current.length) {
    chunks.push(current.join("\n").trimEnd());
  }
  return chunks;
};

export const getPrMeta = ({ prNumber, repoSlug }) => {
  if (null == prNumber || null == repoSlug) {
    return null;
  }
  const { owner, repo } = parseRepoSlug(repoSlug);
  if (null == owner || null == repo) {
    return null;
  }
  const response = runJson("gh", [
    "pr",
    "view",
    String(prNumber),
    "--json",
    "number,url,title,body,baseRefName,headRefName,headRefOid",
    "--repo",
    repoSlug,
  ]);
  return response || null;
};

export const fetchIssueByNumber = ({ issueNumber, repoSlug }) => {
  if (null == issueNumber || null == repoSlug) {
    return null;
  }
  const { owner, repo } = parseRepoSlug(repoSlug);
  if (null == owner || null == repo) {
    return null;
  }
  try {
    return runJson("gh", [
      "api",
      `repos/${owner}/${repo}/issues/${issueNumber}`,
      "-H",
      "Accept: application/vnd.github+json",
    ]);
  } catch (error) {
    const reason = error instanceof Error ? error.message : String(error);
    log(
      `[pr-compare] warning: gh issue fetch failed for ${repoSlug}#${issueNumber}. ${reason}`
    );
    return null;
  }
};

export const getPrDiff = ({ prNumber, repoSlug, contextLines }) => {
  if (null == prNumber || null == repoSlug) {
    return "";
  }
  try {
    const diff = run("gh", [
      "pr",
      "diff",
      String(prNumber),
      "--repo",
      repoSlug,
      "--patch",
    ]);
    return diff || "";
  } catch (error) {
    const reason = error instanceof Error ? error.message : String(error);
    log(`[pr-compare] warning: gh pr diff failed; falling back to local git. ${reason}`);
  }
  return "";
};

export const fetchPrByNumber = ({ prNumber, repoSlug }) => {
  if (null == prNumber || null == repoSlug) {
    return null;
  }
  const { owner, repo } = parseRepoSlug(repoSlug);
  if (null == owner || null == repo) {
    return null;
  }
  return runJson("gh", [
    "api",
    `repos/${owner}/${repo}/pulls/${prNumber}`,
    "-H",
    "Accept: application/vnd.github+json",
  ]);
};

const uniquePrRefs = (entries) => {
  const map = new Map();
  entries.forEach((entry) => {
    if (!entry?.repoSlug || !entry?.prNumber) {
      return;
    }
    const key = `${entry.repoSlug}#${entry.prNumber}`;
    if (!map.has(key)) {
      map.set(key, entry);
    }
  });
  return Array.from(map.values());
};

export const parsePrReference = (value) => {
  if (null == value) {
    return null;
  }
  const text = String(value).trim();
  if ("" === text) {
    return null;
  }
  const urlMatch = text.match(
    /github\.com\/([^/\s]+\/[^/\s]+)\/pull\/(\d+)/i
  );
  if (urlMatch) {
    return { repoSlug: urlMatch[1], prNumber: Number(urlMatch[2]) };
  }
  const hashMatch = text.match(/^([^#\s]+)#[^0-9]*?(\d+)$/);
  if (hashMatch) {
    return { repoSlug: hashMatch[1], prNumber: Number(hashMatch[2]) };
  }
  const slashMatch = text.match(/^([^/\s]+\/[^/\s]+)\/pull\/(\d+)$/);
  if (slashMatch) {
    return { repoSlug: slashMatch[1], prNumber: Number(slashMatch[2]) };
  }
  return null;
};

export const parseRelatedPrArgs = (values = []) => {
  const entries = [];
  const list = Array.isArray(values) ? values : [values];
  list
    .flatMap((value) => String(value).split(","))
    .map((value) => value.trim())
    .filter(Boolean)
    .forEach((value) => {
      const parsed = parsePrReference(value);
      if (parsed?.repoSlug && parsed?.prNumber) {
        entries.push({ ...parsed, source: "arg" });
      }
    });
  return uniquePrRefs(entries);
};

export const extractIssueRefsFromText = ({ text, defaultRepoSlug }) => {
  if (null == text) {
    return [];
  }
  const entries = [];
  const body = String(text);
  const issueUrlRegex =
    /https?:\/\/github\.com\/([^/\s]+\/[^/\s]+)\/issues\/(\d+)/gi;
  let match = issueUrlRegex.exec(body);
  while (match) {
    entries.push({ repoSlug: match[1], issueNumber: Number(match[2]) });
    match = issueUrlRegex.exec(body);
  }
  if (0 === entries.length && defaultRepoSlug) {
    const issueHashRegex = /(^|\s)#(\d{3,})\b/g;
    let hashMatch = issueHashRegex.exec(body);
    while (hashMatch) {
      entries.push({
        repoSlug: defaultRepoSlug,
        issueNumber: Number(hashMatch[2]),
      });
      hashMatch = issueHashRegex.exec(body);
    }
  }
  const deduped = new Map();
  entries.forEach((entry) => {
    if (!entry.repoSlug || !entry.issueNumber) {
      return;
    }
    const key = `${entry.repoSlug}#${entry.issueNumber}`;
    if (!deduped.has(key)) {
      deduped.set(key, entry);
    }
  });
  return Array.from(deduped.values());
};

export const extractPrRefsFromText = (text) => {
  if (null == text) {
    return [];
  }
  const entries = [];
  const body = String(text);
  const prUrlRegex =
    /https?:\/\/github\.com\/([^/\s]+\/[^/\s]+)\/pull\/(\d+)/gi;
  let match = prUrlRegex.exec(body);
  while (match) {
    entries.push({ repoSlug: match[1], prNumber: Number(match[2]) });
    match = prUrlRegex.exec(body);
  }
  return uniquePrRefs(entries);
};

export const resolveRelatedPrs = ({
  prMeta,
  repoSlug,
  explicit = [],
  discover = true,
}) => {
  const related = [];
  const primaryKey = prMeta?.number
    ? `${repoSlug}#${prMeta.number}`
    : null;
  explicit.forEach((entry) => {
    if (entry?.repoSlug && entry?.prNumber) {
      related.push({ ...entry, source: "explicit" });
    }
  });
  if (true === discover && prMeta?.body) {
    const issueRefs = extractIssueRefsFromText({
      text: prMeta.body,
      defaultRepoSlug: repoSlug,
    });
    issueRefs.forEach((issueRef) => {
      const issue = fetchIssueByNumber({
        issueNumber: issueRef.issueNumber,
        repoSlug: issueRef.repoSlug,
      });
      if (!issue?.body) {
        return;
      }
      const prRefs = extractPrRefsFromText(issue.body);
      prRefs.forEach((prRef) => {
        related.push({
          ...prRef,
          source: "issue",
          issue: {
            repoSlug: issueRef.repoSlug,
            issueNumber: issueRef.issueNumber,
          },
        });
      });
    });
  }
  const deduped = uniquePrRefs(related).filter((entry) => {
    if (null == primaryKey) {
      return true;
    }
    return `${entry.repoSlug}#${entry.prNumber}` !== primaryKey;
  });
  return deduped;
};

export const buildTaskContextFromFiles = ({ repoRoot, taskFiles }) => {
  const taskContext = buildTaskContext(repoRoot, taskFiles);
  if (null == taskContext?.implementation_plan_path) {
    return taskContext;
  }
  const normalizedPlanPath = path.normalize(taskContext.implementation_plan_path);
  if (false === fs.existsSync(normalizedPlanPath)) {
    log(`tasks: plan missing ${normalizedPlanPath}`);
  }
  return taskContext;
};
