import { execFile } from "node:child_process";
import { existsSync } from "node:fs";
import { dirname, join, relative, resolve as resolvePath } from "node:path";
import { Type } from "@sinclair/typebox";
import type {
  ExtensionAPI,
  ExtensionCommandContext,
  ExtensionContext,
  ToolResultEvent,
  TurnEndEvent,
} from "@mariozechner/pi-coding-agent";
import { getMarkdownTheme, keyHint } from "@mariozechner/pi-coding-agent";
import type { Theme } from "@mariozechner/pi-tui";
import { Box, Markdown, Text } from "@mariozechner/pi-tui";

const PREVIEW_LINES = 4;
const KNOWLEDGE_STATE_TYPE = "knowledge-session";
const KNOWLEDGE_WIDGET_KEY = "knowledge";
const KNOWLEDGE_REMINDER_INTERVAL_MS = 10 * 60 * 1000;

type Scope = "local" | "global";
type KnowledgeSeverity = "ok" | "watch" | "drift" | "critical";
type KnowledgeToolName = "read" | "write" | "edit";

interface ICliEnvironment {
  executable: string;
  entryPath: string;
  scope: Scope;
}

interface ICommandResult {
  code: number | null;
  stdout: string;
  stderr: string;
}

interface IKnowledgeListImpactedPayload {
  ok: boolean;
  impactedSections: Array<{ id: string; reason: string; confidence: number; filePaths: string[] }>;
  warnings: Array<{ code: string; message: string; confidence: number }>;
  blockingErrors: Array<{ code: string; message: string; confidence: number }>;
  suggestedSectionIds: string[];
  diffStats: {
    codeLines: number;
    knowledgeLines: number;
  };
}

interface IKnowledgeCheckPayload {
  ok: boolean;
  errors: {
    markdown: unknown[];
    codeRefs: unknown[];
    index: unknown[];
    sections: unknown[];
  };
}

interface IKnowledgeSessionState {
  active: boolean;
  filesRead: string[];
  filesEdited: string[];
  filesWritten: string[];
  impactedSectionIds: string[];
  stalenessScore: number;
  severity: KnowledgeSeverity;
  recommendedRefresh: boolean;
  commitWouldFail: boolean;
  diffStats?: {
    codeLines: number;
    knowledgeLines: number;
    observedAt: string;
  };
  lastKnowledgeCheckAt?: string;
  lastSyncSuggestionAt?: string;
  lastReminderAt?: string;
  updatedAt?: string;
}

function findKnowledgeDir(from = process.cwd()): string | null {
  let currentDir = resolvePath(from);
  while (true) {
    const candidate = join(currentDir, "docyrus", "knowledge");
    if (existsSync(candidate)) {
      return candidate;
    }
    const parentDir = dirname(currentDir);
    if (parentDir === currentDir) {
      return null;
    }
    currentDir = parentDir;
  }
}

function readCliEnvironment(env: NodeJS.ProcessEnv = process.env): ICliEnvironment {
  const executable = env.DOCYRUS_CLI_EXECUTABLE?.trim();
  const entryPath = env.DOCYRUS_CLI_ENTRY?.trim();
  const scope = env.DOCYRUS_CLI_SCOPE?.trim() as Scope | undefined;
  if (!executable || !entryPath || (scope !== "local" && scope !== "global")) {
    throw new Error("Missing Docyrus CLI runtime env. Expected DOCYRUS_CLI_EXECUTABLE, DOCYRUS_CLI_ENTRY, and DOCYRUS_CLI_SCOPE.");
  }

  return { executable, entryPath, scope };
}

function buildCommandArgs(environment: ICliEnvironment, args: string[], useJson = false): string[] {
  const scopedArgs = environment.scope === "global" ? ["-g", ...args] : args;
  return [environment.entryPath, ...scopedArgs, ...(useJson ? ["--json"] : [])];
}

function summarizeFailure(result: ICommandResult): string {
  const stderr = result.stderr.trim();
  if (stderr) {
    return stderr;
  }
  const stdout = result.stdout.trim();
  if (stdout) {
    return stdout.split(/\r?\n/u).at(-1) || stdout;
  }
  if (typeof result.code === "number") {
    return `Command exited with code ${result.code}.`;
  }
  return "Command failed.";
}

function runCli(environment: ICliEnvironment, args: string[], cwd: string, useJson = false): Promise<ICommandResult> {
  return new Promise((resolve, reject) => {
    execFile(environment.executable, buildCommandArgs(environment, args, useJson), {
      cwd,
      encoding: "utf8",
      maxBuffer: 20 * 1024 * 1024,
      env: { ...process.env },
    }, (error, stdout, stderr) => {
      if (error && typeof (error as NodeJS.ErrnoException).code === "string" && (error as NodeJS.ErrnoException).code === "ENOENT") {
        reject(error);
        return;
      }
      resolve({
        code: typeof (error as { code?: number } | null)?.code === "number" ? (error as { code?: number }).code || null : null,
        stdout,
        stderr,
      });
    });
  });
}

async function runCliJson<TValue>(environment: ICliEnvironment, args: string[], cwd: string): Promise<{ payload: TValue; raw: ICommandResult }> {
  const result = await runCli(environment, args, cwd, true);
  const output = result.stdout.trim() || result.stderr.trim();
  if (!output) {
    throw new Error("Docyrus CLI produced no JSON output.");
  }
  return {
    payload: JSON.parse(output) as TValue,
    raw: result,
  };
}

function collapsibleResult(
  result: { content: Array<{ type: string; text?: string }> },
  options: { expanded: boolean; isPartial: boolean },
  theme: Theme,
) {
  const text = result.content?.[0]?.type === "text" ? (result.content[0] as { type: "text"; text: string }).text : "";
  if (!text) {
    return new Text(theme.fg("dim", "(empty)"), 0, 0);
  }
  if (options.isPartial) {
    return new Text(theme.fg("dim", "…"), 0, 0);
  }

  const markdownTheme = getMarkdownTheme();
  if (options.expanded) {
    return new Markdown(text, 0, 0, markdownTheme);
  }

  const lines = text.split("\n");
  if (lines.length <= PREVIEW_LINES) {
    return new Markdown(text, 0, 0, markdownTheme);
  }

  const preview = lines.slice(0, PREVIEW_LINES).join("\n");
  const remaining = lines.length - PREVIEW_LINES;
  const hint = keyHint("expandTools", "to expand");
  return new Text(`${preview}\n${theme.fg("dim", `… ${remaining} more lines (${hint})`)}`, 0, 0);
}

function normalizeTrackedPath(pathValue: string, cwd: string): string {
  const trimmed = pathValue.trim();
  if (!trimmed) {
    return trimmed;
  }
  const absolutePath = trimmed.startsWith("/") ? trimmed : join(cwd, trimmed);
  return relative(cwd, absolutePath);
}

function unique(values: string[]): string[] {
  return Array.from(new Set(values)).sort((left, right) => left.localeCompare(right));
}

function defaultKnowledgeState(): IKnowledgeSessionState {
  return {
    active: false,
    filesRead: [],
    filesEdited: [],
    filesWritten: [],
    impactedSectionIds: [],
    stalenessScore: 0,
    severity: "ok",
    recommendedRefresh: false,
    commitWouldFail: false,
  };
}

function computeSeverity(score: number): KnowledgeSeverity {
  if (score >= 75) {
    return "critical";
  }
  if (score >= 45) {
    return "drift";
  }
  if (score >= 20) {
    return "watch";
  }
  return "ok";
}

function scoreKnowledgeState(params: {
  state: IKnowledgeSessionState;
  impacted: IKnowledgeListImpactedPayload;
  knowledgeCheckOk?: boolean;
}): { score: number; severity: KnowledgeSeverity; commitWouldFail: boolean; recommendedRefresh: boolean } {
  let score = 0;
  score += Math.min(25, params.impacted.diffStats.codeLines / 4);
  if (params.impacted.diffStats.knowledgeLines === 0 && params.impacted.diffStats.codeLines > 0) {
    score += 15;
  }
  score += Math.min(20, params.impacted.impactedSections.length * 6);
  score += Math.min(20, params.impacted.warnings.length * 8);
  score += params.impacted.blockingErrors.length > 0 ? 40 : 0;
  score += Math.min(10, params.state.filesEdited.length * 2);
  score += Math.min(5, params.state.filesWritten.length);
  if (params.knowledgeCheckOk === false) {
    score += 35;
  }
  const normalizedScore = Math.max(0, Math.min(100, Math.round(score)));
  const severity = computeSeverity(normalizedScore);
  return {
    score: normalizedScore,
    severity,
    commitWouldFail: params.impacted.blockingErrors.length > 0 || params.knowledgeCheckOk === false || severity === "critical",
    recommendedRefresh: severity === "drift" || severity === "critical",
  };
}

function shouldNotifyKnowledgeReminder(previous: IKnowledgeSessionState, next: IKnowledgeSessionState): boolean {
  if (!next.recommendedRefresh) {
    return false;
  }
  const previousRank = previous.severity === "critical" ? 3 : previous.severity === "drift" ? 2 : previous.severity === "watch" ? 1 : 0;
  const nextRank = next.severity === "critical" ? 3 : next.severity === "drift" ? 2 : next.severity === "watch" ? 1 : 0;
  if (nextRank > previousRank) {
    return true;
  }
  if (!next.lastReminderAt) {
    return true;
  }
  return Date.now() - new Date(next.lastReminderAt).getTime() >= KNOWLEDGE_REMINDER_INTERVAL_MS;
}

function getLatestKnowledgeState(ctx: ExtensionContext): IKnowledgeSessionState {
  const entries = ctx.sessionManager.getEntries();
  for (let index = entries.length - 1; index >= 0; index -= 1) {
    const entry = entries[index] as { type: string; customType?: string; data?: IKnowledgeSessionState };
    if (entry.type === "custom" && entry.customType === KNOWLEDGE_STATE_TYPE && entry.data) {
      return entry.data;
    }
  }
  return defaultKnowledgeState();
}

function persistKnowledgeState(pi: ExtensionAPI, state: IKnowledgeSessionState): void {
  pi.appendEntry(KNOWLEDGE_STATE_TYPE, state);
}

function updateKnowledgeWidget(ctx: ExtensionContext, state: IKnowledgeSessionState): void {
  if (!ctx.hasUI) {
    return;
  }
  if (!state.active) {
    ctx.ui.setWidget(KNOWLEDGE_WIDGET_KEY, undefined);
    return;
  }

  const label = state.commitWouldFail
    ? `Knowledge: commit blocked (${state.stalenessScore})`
    : state.recommendedRefresh
      ? `Knowledge: refresh suggested (${state.stalenessScore})`
      : `Knowledge: in sync (${state.stalenessScore})`;
  const color = state.commitWouldFail ? "warning" : state.recommendedRefresh ? "accent" : "success";
  ctx.ui.setWidget(KNOWLEDGE_WIDGET_KEY, [ctx.ui.theme.fg(color as never, label)]);
}

function summarizeStatus(state: IKnowledgeSessionState): string {
  const parts = [
    `severity: ${state.severity}`,
    `score: ${state.stalenessScore}`,
    `recommendedRefresh: ${state.recommendedRefresh}`,
    `commitWouldFail: ${state.commitWouldFail}`,
    `impactedSections: ${state.impactedSectionIds.length > 0 ? state.impactedSectionIds.join(", ") : "(none)"}`,
    `filesRead: ${state.filesRead.length}`,
    `filesEdited: ${state.filesEdited.length}`,
    `filesWritten: ${state.filesWritten.length}`,
  ];
  if (state.diffStats) {
    parts.push(`diffStats: code=${state.diffStats.codeLines}, knowledge=${state.diffStats.knowledgeLines}`);
  }
  if (state.lastKnowledgeCheckAt) {
    parts.push(`lastKnowledgeCheckAt: ${state.lastKnowledgeCheckAt}`);
  }
  return parts.join("\n");
}

async function refreshKnowledgeState(
  pi: ExtensionAPI,
  ctx: ExtensionContext,
  options?: {
    runCheck?: boolean;
    allowReminder?: boolean;
  },
): Promise<IKnowledgeSessionState> {
  const knowledgeDir = findKnowledgeDir(ctx.cwd);
  if (!knowledgeDir) {
    const inactive = defaultKnowledgeState();
    updateKnowledgeWidget(ctx, inactive);
    persistKnowledgeState(pi, inactive);
    return inactive;
  }

  const previous = getLatestKnowledgeState(ctx);
  const environment = readCliEnvironment();
  const impacted = (await runCliJson<IKnowledgeListImpactedPayload>(environment, ["knowledge", "list-impacted"], ctx.cwd)).payload;
  let knowledgeCheckOk: boolean | undefined;
  const now = new Date().toISOString();

  if (options?.runCheck) {
    const check = (await runCliJson<IKnowledgeCheckPayload>(environment, ["knowledge", "check"], ctx.cwd)).payload;
    knowledgeCheckOk = check.ok;
  }

  const scored = scoreKnowledgeState({
    state: previous,
    impacted,
    knowledgeCheckOk,
  });

  const next: IKnowledgeSessionState = {
    active: true,
    filesRead: previous.filesRead,
    filesEdited: previous.filesEdited,
    filesWritten: previous.filesWritten,
    impactedSectionIds: impacted.suggestedSectionIds,
    stalenessScore: scored.score,
    severity: scored.severity,
    recommendedRefresh: scored.recommendedRefresh,
    commitWouldFail: scored.commitWouldFail,
    diffStats: {
      codeLines: impacted.diffStats.codeLines,
      knowledgeLines: impacted.diffStats.knowledgeLines,
      observedAt: now,
    },
    lastKnowledgeCheckAt: knowledgeCheckOk ? now : previous.lastKnowledgeCheckAt,
    lastSyncSuggestionAt: previous.lastSyncSuggestionAt,
    lastReminderAt: previous.lastReminderAt,
    updatedAt: now,
  };

  if (options?.allowReminder && shouldNotifyKnowledgeReminder(previous, next)) {
    next.lastReminderAt = now;
    next.lastSyncSuggestionAt = now;
    if (ctx.hasUI) {
      ctx.ui.notify(
        next.commitWouldFail
          ? "Knowledge drift is severe enough that commit-time checks would likely fail. Run /knowledge-refresh."
          : "Knowledge drift detected. Run /knowledge-refresh to update the likely impacted sections.",
        next.commitWouldFail ? "warning" : "info",
      );
    }
  }

  persistKnowledgeState(pi, next);
  updateKnowledgeWidget(ctx, next);
  return next;
}

function trackToolResult(pi: ExtensionAPI, ctx: ExtensionContext, event: ToolResultEvent): void {
  if (event.isError) {
    return;
  }
  if (event.toolName !== "read" && event.toolName !== "write" && event.toolName !== "edit") {
    return;
  }
  const inputPath = typeof event.input.path === "string" ? event.input.path : "";
  if (!inputPath) {
    return;
  }

  const state = getLatestKnowledgeState(ctx);
  const normalized = normalizeTrackedPath(inputPath, ctx.cwd);
  const next: IKnowledgeSessionState = {
    ...state,
    active: Boolean(findKnowledgeDir(ctx.cwd)),
    filesRead: event.toolName === "read" ? unique([...state.filesRead, normalized]) : state.filesRead,
    filesEdited: event.toolName === "edit" ? unique([...state.filesEdited, normalized]) : state.filesEdited,
    filesWritten: event.toolName === "write" ? unique([...state.filesWritten, normalized]) : state.filesWritten,
    updatedAt: new Date().toISOString(),
  };
  persistKnowledgeState(pi, next);
  updateKnowledgeWidget(ctx, next);
}

function registerKnowledgeTool(
  pi: ExtensionAPI,
  name: string,
  label: string,
  description: string,
  argsBuilder: (params: Record<string, unknown>) => string[],
  schema: object,
  preview: (args: Record<string, unknown>, theme: Theme) => string,
) {
  pi.registerTool({
    name,
    label,
    description,
    promptSnippet: description,
    parameters: schema,
    async execute(_id, params) {
      const environment = readCliEnvironment();
      const result = await runCli(environment, argsBuilder(params as Record<string, unknown>), process.cwd(), false);
      const text = result.stdout.trim() || summarizeFailure(result);
      return {
        content: [{ type: "text", text }],
        ...(result.code && result.code !== 0 ? { isError: true } : {}),
      };
    },
    renderCall(args, theme) {
      return new Text(preview(args as Record<string, unknown>, theme), 0, 0);
    },
    renderResult: collapsibleResult,
  });
}

function parseCommandArgs(rawArgs: string): string | null {
  const trimmed = rawArgs.trim();
  return trimmed.length > 0 ? trimmed : null;
}

export default function(pi: ExtensionAPI) {
  registerKnowledgeTool(
    pi,
    "docyrus_knowledge_search",
    "knowledge search",
    "Semantic search across docyrus/knowledge sections",
    (params) => {
      const args = ["knowledge", "search"];
      if (typeof params.query === "string" && params.query.trim().length > 0) {
        args.push(params.query);
      }
      if (typeof params.limit === "number") {
        args.push("--limit", String(params.limit));
      }
      return args;
    },
    Type.Object({
      query: Type.String({ description: "Search query in natural language" }),
      limit: Type.Optional(Type.Number({ description: "Maximum matches", default: 5 })),
    }),
    (args, theme) => `${theme.fg("toolTitle", theme.bold("knowledge search "))}${theme.fg("dim", `"${String(args.query || "")}"`)}`,
  );

  registerKnowledgeTool(
    pi,
    "docyrus_knowledge_section",
    "knowledge section",
    "Show a full knowledge section with refs and backlinks",
    (params) => ["knowledge", "section", String(params.query || "")],
    Type.Object({
      query: Type.String({ description: "Section id or heading query" }),
    }),
    (args, theme) => `${theme.fg("toolTitle", theme.bold("knowledge section "))}${theme.fg("dim", `"${String(args.query || "")}"`)}`,
  );

  registerKnowledgeTool(
    pi,
    "docyrus_knowledge_locate",
    "knowledge locate",
    "Find knowledge sections by exact, short, or fuzzy match",
    (params) => ["knowledge", "locate", String(params.query || "")],
    Type.Object({
      query: Type.String({ description: "Section id or heading query" }),
    }),
    (args, theme) => `${theme.fg("toolTitle", theme.bold("knowledge locate "))}${theme.fg("dim", `"${String(args.query || "")}"`)}`,
  );

  registerKnowledgeTool(
    pi,
    "docyrus_knowledge_refs",
    "knowledge refs",
    "Find markdown and code references to a knowledge section or source symbol",
    (params) => {
      const args = ["knowledge", "refs", String(params.query || "")];
      if (typeof params.scope === "string" && params.scope.length > 0) {
        args.push("--scope", params.scope);
      }
      return args;
    },
    Type.Object({
      query: Type.String({ description: "Section id, source file, or source symbol query" }),
      scope: Type.Optional(Type.String({ description: "md, code, or md+code" })),
    }),
    (args, theme) => `${theme.fg("toolTitle", theme.bold("knowledge refs "))}${theme.fg("dim", `"${String(args.query || "")}"`)}`,
  );

  registerKnowledgeTool(
    pi,
    "docyrus_knowledge_expand",
    "knowledge expand",
    "Expand [[refs]] into resolved section context",
    (params) => ["knowledge", "expand", String(params.text || "")],
    Type.Object({
      text: Type.String({ description: "Text containing [[refs]]" }),
    }),
    (args, theme) => {
      const text = String(args.text || "");
      const preview = text.length > 60 ? `${text.slice(0, 60)}…` : text;
      return `${theme.fg("toolTitle", theme.bold("knowledge expand "))}${theme.fg("dim", `"${preview}"`)}`;
    },
  );

  registerKnowledgeTool(
    pi,
    "docyrus_knowledge_check",
    "knowledge check",
    "Validate the repo knowledge graph",
    () => ["knowledge", "check"],
    Type.Object({}),
    (_args, theme) => `${theme.fg("toolTitle", theme.bold("knowledge check"))}`,
  );

  pi.registerCommand("knowledge-init", {
    description: "Bootstrap docyrus/knowledge from the current repo and install hook guidance",
    handler: async(rawArgs, ctx: ExtensionCommandContext) => {
      const environment = readCliEnvironment();
      const brief = parseCommandArgs(rawArgs);
      const args = ["knowledge", "init", ...(brief ? ["--brief", brief] : [])];
      const result = await runCliJson<Record<string, unknown>>(environment, args, ctx.cwd);
      if (ctx.hasUI) {
        ctx.ui.notify(`Knowledge init complete: ${result.payload.knowledgeDir || "docyrus/knowledge/"}`, "info");
      }
      await refreshKnowledgeState(pi, ctx, {
        runCheck: true,
        allowReminder: false,
      });
    },
  });

  pi.registerCommand("knowledge-status", {
    description: "Show current knowledge staleness status and likely impacted sections",
    handler: async(_rawArgs, ctx: ExtensionCommandContext) => {
      const state = await refreshKnowledgeState(pi, ctx, {
        runCheck: true,
        allowReminder: false,
      });
      if (!ctx.hasUI) {
        return;
      }
      ctx.ui.notify(summarizeStatus(state), state.commitWouldFail ? "warning" : "info");
    },
  });

  pi.registerCommand("knowledge-refresh", {
    description: "Refresh the managed knowledge files for the likely impacted sections",
    handler: async(rawArgs, ctx: ExtensionCommandContext) => {
      const environment = readCliEnvironment();
      const state = await refreshKnowledgeState(pi, ctx, {
        runCheck: false,
        allowReminder: false,
      });
      const brief = parseCommandArgs(rawArgs);
      const args = ["knowledge", "refresh"];
      if (state.impactedSectionIds.length > 0) {
        args.push("--sections", state.impactedSectionIds.join(","));
      }
      if (brief) {
        args.push(brief);
      }
      const result = await runCliJson<Record<string, unknown>>(environment, args, ctx.cwd);
      if (ctx.hasUI) {
        ctx.ui.notify(`Knowledge refresh complete for ${state.impactedSectionIds.length > 0 ? state.impactedSectionIds.length : "default"} target(s).`, "info");
      }
      const nextState = await refreshKnowledgeState(pi, ctx, {
        runCheck: true,
        allowReminder: false,
      });
      persistKnowledgeState(pi, {
        ...nextState,
        lastSyncSuggestionAt: new Date().toISOString(),
      });
      void result;
    },
  });

  pi.on("tool_result", (event: ToolResultEvent, ctx: ExtensionContext) => {
    trackToolResult(pi, ctx, event);
  });

  pi.on("session_start", async(_event, ctx) => {
    await refreshKnowledgeState(pi, ctx, {
      runCheck: false,
      allowReminder: true,
    });
  });

  pi.on("session_switch", async(_event, ctx) => {
    await refreshKnowledgeState(pi, ctx, {
      runCheck: false,
      allowReminder: true,
    });
  });

  pi.on("turn_end", async(_event: TurnEndEvent, ctx) => {
    await refreshKnowledgeState(pi, ctx, {
      runCheck: false,
      allowReminder: true,
    });
  });

  pi.on("agent_end", async(_event, ctx) => {
    const state = await refreshKnowledgeState(pi, ctx, {
      runCheck: true,
      allowReminder: true,
    });
    if (ctx.hasUI && state.commitWouldFail) {
      ctx.ui.notify("Knowledge drift is severe or integrity checks failed. A commit would likely be blocked until knowledge is updated.", "warning");
    }
  });

  pi.on("session_shutdown", async(_event, ctx) => {
    updateKnowledgeWidget(ctx, defaultKnowledgeState());
  });

  pi.registerMessageRenderer("knowledge-reminder", (message, { expanded }, theme) => {
    const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
    if (expanded) {
      box.addChild(new Text(theme.fg("accent", "Knowledge"), 0, 0));
      box.addChild(new Markdown(message.content, 0, 0, getMarkdownTheme()));
      return box;
    }

    const hint = keyHint("expandTools", "to expand");
    box.addChild(new Text(`${theme.fg("accent", "Knowledge")} ${theme.fg("dim", `Watch the knowledge widget and use /knowledge-refresh when drift appears. (${hint})`)}`, 0, 0));
    return box;
  });
}
