import { readFile, stat } from "node:fs/promises";
import path from "node:path";

import type { TaskStatus } from "./contracts.ts";

export type MdtodoReportSource = { href: string; sourcePath: string; body: string; modifiedAt: string };
export type MdtodoImportPayload = {
  sourcePath: string;
  sourceModifiedAt: string;
  markdown: string;
  reports: MdtodoReportSource[];
  groupName?: string;
  dryRun?: boolean;
};

export type MdtodoImportTask = {
  sourceId: string;
  parentSourceId: string | null;
  title: string;
  description: string;
  status: TaskStatus;
  startAt: string;
  dueAt: string;
  reports: Array<{ title: string; body: string; status: string; createdAt: string }>;
};

export type MdtodoImportPlan = {
  sourcePath: string;
  groupName: string;
  groupDescription: string;
  parserMode: "rxx" | "legacy-checkbox";
  tasks: MdtodoImportTask[];
  summary: {
    taskCount: number;
    subtaskCount: number;
    subsubtaskCount: number;
    reportCount: number;
    missingReportCount: number;
    flattenedTaskCount: number;
    warningCount: number;
  };
  warnings: Array<{ code: string; message: string; sourceId?: string; href?: string }>;
};

const rxxHeadingPattern = /^(#{2,})\s+(R\d+(?:\.\d+)*)(?:\s+(.*?))?\s*$/iu;
const checkboxPattern = /^(\s*)(?:[-*+]|\d+[.)])\s+\[([ xX-])\]\s+(.*)$/u;
const markdownLinkPattern = /\[([^\]]*)\]\(([^)]+)\)/gu;

export async function loadMdtodoImportPayload(filePath: string, options: { groupName?: string; dryRun?: boolean } = {}): Promise<MdtodoImportPayload> {
  const sourcePath = path.resolve(filePath);
  const [markdown, sourceInfo] = await Promise.all([readFile(sourcePath, "utf8"), stat(sourcePath)]);
  const reports: MdtodoReportSource[] = [];
  const seen = new Set<string>();
  for (const link of reportLinks(markdown)) {
    const reportPath = path.resolve(path.dirname(sourcePath), link.href);
    if (seen.has(reportPath)) continue;
    seen.add(reportPath);
    try {
      const [body, reportInfo] = await Promise.all([readFile(reportPath, "utf8"), stat(reportPath)]);
      reports.push({ href: link.href, sourcePath: reportPath, body, modifiedAt: reportInfo.mtime.toISOString() });
    } catch {
      // Missing report files are reported by the pure parser so dry-run and import agree.
    }
  }
  return { sourcePath, sourceModifiedAt: sourceInfo.mtime.toISOString(), markdown, reports, groupName: options.groupName, dryRun: options.dryRun };
}

export function parseMdtodoImport(payload: MdtodoImportPayload): MdtodoImportPlan {
  const lines = String(payload.markdown ?? "").split(/\r?\n/u);
  const rxxBlocks = collectRxxBlocks(lines);
  const parsed = rxxBlocks.length ? parseRxx(lines, rxxBlocks, payload) : parseCheckboxes(lines, payload);
  if (!parsed.tasks.length) throw codedError("mdtodo_tasks_not_found", "MDTODO file does not contain Rxx headings or checkbox tasks");
  const reportCount = parsed.tasks.reduce((count, task) => count + task.reports.length, 0);
  const missingReportCount = parsed.warnings.filter((warning) => warning.code === "report_file_missing").length;
  const flattenedTaskCount = parsed.warnings.filter((warning) => warning.code === "nested_task_flattened").length;
  const tasksBySourceId = new Map(parsed.tasks.map((task) => [task.sourceId, task]));
  return {
    sourcePath: payload.sourcePath,
    groupName: cleanTitle(payload.groupName || documentTitle(lines, payload.sourcePath)),
    groupDescription: `Imported from MDTODO: ${payload.sourcePath}`,
    parserMode: rxxBlocks.length ? "rxx" : "legacy-checkbox",
    tasks: parsed.tasks,
    summary: {
      taskCount: parsed.tasks.filter((task) => !task.parentSourceId).length,
      subtaskCount: parsed.tasks.filter((task) => taskHierarchyDepth(task, tasksBySourceId) === 1).length,
      subsubtaskCount: parsed.tasks.filter((task) => taskHierarchyDepth(task, tasksBySourceId) === 2).length,
      reportCount,
      missingReportCount,
      flattenedTaskCount,
      warningCount: parsed.warnings.length
    },
    warnings: parsed.warnings.slice(0, 100)
  };
}

function parseRxx(lines: string[], blocks: RxxBlock[], payload: MdtodoImportPayload) {
  const tasks: MdtodoImportTask[] = [];
  const warnings: MdtodoImportPlan["warnings"] = [];
  const sourceIds = new Set(blocks.map((block) => block.sourceId));
  for (const block of blocks) {
    const depth = rxxDepth(block.sourceId);
    const supportedSourceId = depth > 2 ? block.sourceId.split(".").slice(0, 3).join(".") : block.sourceId;
    const parentSourceId = nearestRxxParent(supportedSourceId, sourceIds);
    if (depth > 2) warnings.push({ code: "nested_task_flattened", sourceId: block.sourceId, message: `${block.sourceId} was flattened to the supported third level under ${parentSourceId ?? "the taskgroup"}` });
    if (depth > 0 && !parentSourceId) warnings.push({ code: "parent_task_missing", sourceId: block.sourceId, message: `${block.sourceId} has no importable parent task and was imported as a task` });
    const description = lines.slice(block.lineIndex + 1, block.endLine).join("\n").trim();
    const reports = reportsForTask(description, payload, block.sourceId, warnings);
    tasks.push({
      sourceId: block.sourceId,
      parentSourceId,
      title: cleanTitle(`${block.sourceId} ${block.title || conciseBodyTitle(description)}`),
      description,
      status: block.status,
      ...importedTaskRange(payload.sourceModifiedAt, reports),
      reports
    });
  }
  expandParentRanges(tasks);
  return { tasks, warnings };
}

function parseCheckboxes(lines: string[], payload: MdtodoImportPayload) {
  const tasks: MdtodoImportTask[] = [];
  const warnings: MdtodoImportPlan["warnings"] = [];
  const parentsByDepth: string[] = [];
  for (let index = 0; index < lines.length; index += 1) {
    const match = checkboxPattern.exec(lines[index]);
    if (!match) continue;
    const depth = Math.max(0, Math.floor(match[1].replace(/\t/gu, "  ").length / 2));
    const sourceId = `L${index + 1}`;
    const supportedDepth = Math.min(depth, 2);
    const parentSourceId = supportedDepth > 0 ? parentsByDepth[supportedDepth - 1] ?? null : null;
    if (depth > 2) warnings.push({ code: "nested_task_flattened", sourceId, message: `${sourceId} was flattened to the supported third checkbox level` });
    tasks.push({
      sourceId,
      parentSourceId,
      title: cleanTitle(match[3]),
      description: "",
      status: match[2].toLowerCase() === "x" ? "completed" : match[2] === "-" ? "blocked" : "pending",
      ...importedTaskRange(payload.sourceModifiedAt, []),
      reports: []
    });
    parentsByDepth[supportedDepth] = sourceId;
    parentsByDepth.length = supportedDepth + 1;
  }
  expandParentRanges(tasks);
  return { tasks, warnings };
}

function reportsForTask(body: string, payload: MdtodoImportPayload, sourceId: string, warnings: MdtodoImportPlan["warnings"]) {
  const reports: MdtodoImportTask["reports"] = [];
  for (const link of reportLinks(body)) {
    const resolved = path.resolve(path.dirname(payload.sourcePath), link.href);
    const report = payload.reports.find((candidate) => path.resolve(candidate.sourcePath) === resolved);
    if (!report) {
      warnings.push({ code: "report_file_missing", sourceId, href: link.href, message: `Report file could not be read: ${link.href}` });
      continue;
    }
    reports.push({ title: documentTitle(report.body.split(/\r?\n/u), report.sourcePath), body: report.body, status: "imported", createdAt: requiredIsoDate(report.modifiedAt, "report.modifiedAt") });
  }
  return reports;
}

function importedTaskRange(sourceModifiedAt: string, reports: MdtodoImportTask["reports"]) {
  const sourceTime = new Date(requiredIsoDate(sourceModifiedAt, "sourceModifiedAt")).getTime();
  const reportTimes = reports.map((report) => new Date(report.createdAt).getTime());
  const dueTime = reportTimes.length ? Math.max(...reportTimes) : sourceTime;
  return { startAt: new Date(dueTime - 86400000).toISOString(), dueAt: new Date(dueTime).toISOString() };
}

function expandParentRanges(tasks: MdtodoImportTask[]) {
  const tasksBySourceId = new Map(tasks.map((task) => [task.sourceId, task]));
  const deepestFirst = [...tasks].sort((left, right) => taskHierarchyDepth(right, tasksBySourceId) - taskHierarchyDepth(left, tasksBySourceId));
  for (const child of deepestFirst) {
    if (!child.parentSourceId) continue;
    const parent = tasksBySourceId.get(child.parentSourceId);
    if (!parent) continue;
    const startAt = Math.min(new Date(parent.startAt).getTime(), new Date(child.startAt).getTime());
    const dueAt = Math.max(new Date(parent.dueAt).getTime(), new Date(child.dueAt).getTime());
    parent.startAt = new Date(startAt).toISOString();
    parent.dueAt = new Date(dueAt).toISOString();
  }
}

function nearestRxxParent(sourceId: string, sourceIds: Set<string>) {
  const parts = sourceId.split(".");
  while (parts.length > 1) {
    parts.pop();
    const candidate = parts.join(".");
    if (sourceIds.has(candidate)) return candidate;
  }
  return null;
}

function taskHierarchyDepth(task: MdtodoImportTask, tasksBySourceId: Map<string, MdtodoImportTask>) {
  let depth = 0;
  let parentSourceId = task.parentSourceId;
  while (parentSourceId && depth < 3) {
    depth += 1;
    parentSourceId = tasksBySourceId.get(parentSourceId)?.parentSourceId ?? null;
  }
  return depth;
}

function reportLinks(markdown: string) {
  const links: Array<{ label: string; href: string }> = [];
  for (const match of String(markdown ?? "").matchAll(markdownLinkPattern)) {
    const label = String(match[1] ?? "").trim();
    const href = String(match[2] ?? "").trim().replace(/^<|>$/gu, "").split(/\s+/u)[0] ?? "";
    if (!href || !/\.md(?:#.*)?$/iu.test(href) || /^(?:[a-z][a-z0-9+.-]*:|\/|[A-Za-z]:\/)/iu.test(href)) continue;
    if (!/(?:任务报告|task[_ -]?report|report)/iu.test(`${label} ${href}`)) continue;
    links.push({ label, href: href.split("#")[0] });
  }
  return links;
}

type RxxBlock = { sourceId: string; title: string; status: TaskStatus; lineIndex: number; endLine: number };
function collectRxxBlocks(lines: string[]): RxxBlock[] {
  const blocks: Omit<RxxBlock, "endLine">[] = [];
  for (let index = 0; index < lines.length; index += 1) {
    const match = rxxHeadingPattern.exec(lines[index]);
    if (!match) continue;
    const rest = String(match[3] ?? "").trim();
    blocks.push({ sourceId: match[2].replace(/^r/iu, "R"), title: stripStatus(rest), status: statusFrom(rest), lineIndex: index });
  }
  return blocks.map((block, index) => ({ ...block, endLine: blocks[index + 1]?.lineIndex ?? lines.length }));
}

function statusFrom(value: string): TaskStatus {
  const markers = [...value.matchAll(/\[([^\]]+)\]/gu)].map((match) => String(match[1]).trim().toLowerCase().replace(/[\s-]+/gu, "_"));
  if (markers.some((marker) => marker === "blocked")) return "blocked";
  if (markers.some((marker) => ["completed", "finished", "done"].includes(marker))) return "completed";
  if (markers.some((marker) => ["in_progress", "processing"].includes(marker))) return "in_progress";
  return "pending";
}
function stripStatus(value: string) { return value.replace(/\s*\[(?:open|todo|blocked|in[_ -]?progress|processing|completed|finished|done)\]\s*/giu, " ").trim(); }
function rxxDepth(sourceId: string) { return Math.max(0, sourceId.split(".").length - 1); }
function documentTitle(lines: string[], sourcePath: string) { return cleanTitle(lines.find((line) => /^#{1,6}\s+\S/u.test(line))?.replace(/^#{1,6}\s+/u, "") || path.basename(sourcePath, path.extname(sourcePath))); }
function firstBodyLine(body: string) { return body.split(/\r?\n/u).map((line) => line.trim()).find(Boolean) || "Untitled task"; }
function conciseBodyTitle(body: string) {
  return firstBodyLine(body);
}
function cleanTitle(value: string) { return String(value ?? "").replace(/\s+<!--.*?-->\s*$/u, "").trim().slice(0, 500) || "Untitled task"; }
function requiredIsoDate(value: unknown, field: string) { const date = new Date(String(value ?? "")); if (Number.isNaN(date.valueOf())) throw codedError("invalid_mdtodo_file_time", `${field} must be an ISO date`); return date.toISOString(); }
function codedError(code: string, message: string) { return Object.assign(new Error(message), { code }); }
