import { existsSync, readFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { homedir } from "node:os";
import type { ThinkingLevel } from "@mariozechner/pi-agent-core";

export type PhaseName = "planning" | "executing" | "reviewing";
export type RuntimePhase = PhaseName | "idle";

export interface PhaseModelRef {
  provider: string;
  id: string;
}

/**
 * Config values loaded from JSON can intentionally clear inherited values.
 *
 * - `null` clears a value from a parent config.
 * - `[]` clears active tools.
 * - `""` clears string values.
 */
export interface PhaseProfile {
  model?: PhaseModelRef | null;
  thinking?: ThinkingLevel | null;
  activeTools?: string[] | null;
  statusLabel?: string | null;
  systemPrompt?: string | null;
}

export interface PlannotatorConfig {
  defaults?: PhaseProfile | null;
  phases?: Partial<Record<PhaseName, PhaseProfile | null>>;
}

export interface LoadedPlannotatorConfig {
  config: PlannotatorConfig;
  warnings: string[];
}

export interface ResolvedPhaseProfile {
  model?: PhaseModelRef;
  thinking?: ThinkingLevel;
  activeTools?: string[];
  statusLabel?: string;
  systemPrompt?: string;
}

export interface PromptVariables {
  planFilePath: string;
  todoList: string;
  completedCount: number;
  totalCount: number;
  remainingCount: number;
  phase: RuntimePhase;
}

export interface PromptRenderResult {
  text: string;
  unknownVariables: string[];
}

const INTERNAL_CONFIG_PATH = join(dirname(fileURLToPath(import.meta.url)), "plannotator.json");
const PHASES: PhaseName[] = ["planning", "executing", "reviewing"];
const THINKING_LEVELS = new Set<string>(["minimal", "low", "medium", "high", "xhigh"]);

function getAgentConfigDir(): string {
  const envDir = process.env.PI_CODING_AGENT_DIR;
  if (envDir) return envDir;
  return join(process.env.HOME || process.env.USERPROFILE || homedir(), ".pi", "agent");
}

function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === "object" && value !== null && !Array.isArray(value);
}

function readJsonFile(path: string): { data?: unknown; error?: string } {
  if (!existsSync(path)) return {};

  try {
    return { data: JSON.parse(readFileSync(path, "utf-8")) };
  } catch (error) {
    return { error: `Failed to parse ${path}: ${error instanceof Error ? error.message : String(error)}` };
  }
}

function normalizeModel(value: unknown): PhaseModelRef | null | undefined {
  if (value === null) return null;
  if (!isRecord(value)) return undefined;

  const provider = typeof value.provider === "string" ? value.provider.trim() : "";
  const id = typeof value.id === "string" ? value.id.trim() : "";
  if (!provider || !id) return undefined;
  return { provider, id };
}

function normalizeThinking(value: unknown): ThinkingLevel | null | undefined {
  if (value === null) return null;
  if (typeof value !== "string") return undefined;
  const trimmed = value.trim();
  if (!trimmed) return null;

  return THINKING_LEVELS.has(trimmed as ThinkingLevel) ? (trimmed as ThinkingLevel) : undefined;
}

function normalizeTools(value: unknown): string[] | null | undefined {
  if (value === null) return null;
  if (!Array.isArray(value)) return undefined;
  if (value.length === 0) return [];

  const tools = value.filter((tool): tool is string => typeof tool === "string" && tool.trim().length > 0);
  return tools.length > 0 ? tools : undefined;
}

function normalizeLabel(value: unknown): string | null | undefined {
  if (value === null) return null;
  if (typeof value !== "string") return undefined;
  const trimmed = value.trim();
  return trimmed.length > 0 ? trimmed : null;
}

function normalizePrompt(value: unknown): string | null | undefined {
  if (value === null) return null;
  if (typeof value !== "string") return undefined;
  return value.length > 0 ? value : null;
}

function normalizeProfile(raw: unknown): PhaseProfile | null | undefined {
  if (raw === null) return null;
  if (!isRecord(raw)) return undefined;

  const profile: PhaseProfile = {};

  if ("model" in raw) profile.model = normalizeModel(raw.model);
  if ("thinking" in raw) profile.thinking = normalizeThinking(raw.thinking);
  if ("thinkingLevel" in raw && profile.thinking === undefined) profile.thinking = normalizeThinking(raw.thinkingLevel);
  if ("activeTools" in raw) profile.activeTools = normalizeTools(raw.activeTools);
  if ("statusLabel" in raw) profile.statusLabel = normalizeLabel(raw.statusLabel);
  if ("systemPrompt" in raw) profile.systemPrompt = normalizePrompt(raw.systemPrompt);

  return profile;
}

function cloneProfile(profile: PhaseProfile | null | undefined): PhaseProfile | null | undefined {
  if (profile === null || profile === undefined) return profile;
  return { ...profile, activeTools: profile.activeTools ? [...profile.activeTools] : profile.activeTools };
}

function mergeProfile(base: PhaseProfile | null | undefined, override: PhaseProfile | null | undefined): PhaseProfile | null | undefined {
  if (override === null) return null;
  if (override === undefined) return cloneProfile(base);
  if (base === null || base === undefined) return cloneProfile(override);

  const merged: PhaseProfile = {
    model: override.model !== undefined ? override.model : base.model,
    thinking: override.thinking !== undefined ? override.thinking : base.thinking,
    activeTools: override.activeTools !== undefined ? override.activeTools : base.activeTools,
    statusLabel: override.statusLabel !== undefined ? override.statusLabel : base.statusLabel,
    systemPrompt: override.systemPrompt !== undefined ? override.systemPrompt : base.systemPrompt,
  };

  return merged;
}

function mergeConfig(base: PlannotatorConfig, override: PlannotatorConfig): PlannotatorConfig {
  const phases: Partial<Record<PhaseName, PhaseProfile | null>> = {};
  for (const phase of PHASES) {
    const merged = mergeProfile(base.phases?.[phase], override.phases?.[phase]);
    if (merged !== undefined) phases[phase] = merged;
  }

  return {
    defaults: mergeProfile(base.defaults, override.defaults),
    phases: Object.keys(phases).length > 0 ? phases : undefined,
  };
}

function loadConfigSource(path: string): { config: PlannotatorConfig; warning?: string } {
  const parsed = readJsonFile(path);
  if (parsed.error) {
    return { config: {}, warning: parsed.error };
  }

  const raw = parsed.data;
  if (!isRecord(raw)) return { config: {} };

  const config: PlannotatorConfig = {};
  if ("defaults" in raw) config.defaults = normalizeProfile(raw.defaults);

  if ("phases" in raw && isRecord(raw.phases)) {
    const phases: Partial<Record<PhaseName, PhaseProfile | null>> = {};
    for (const phase of PHASES) {
      const normalized = normalizeProfile(raw.phases[phase]);
      if (normalized !== undefined) phases[phase] = normalized;
    }
    if (Object.keys(phases).length > 0) config.phases = phases;
  }

  return { config };
}

export function loadPlannotatorConfig(cwd: string): LoadedPlannotatorConfig {
  const warnings: string[] = [];

  const internal = loadConfigSource(INTERNAL_CONFIG_PATH);
  if (internal.warning) warnings.push(internal.warning);

  const globalPath = join(getAgentConfigDir(), "plannotator.json");
  const globalConfig = loadConfigSource(globalPath);
  if (globalConfig.warning) warnings.push(globalConfig.warning);

  const projectPath = join(cwd, ".pi", "plannotator.json");
  const projectConfig = loadConfigSource(projectPath);
  if (projectConfig.warning) warnings.push(projectConfig.warning);

  const merged = mergeConfig(mergeConfig(internal.config, globalConfig.config), projectConfig.config);
  return { config: merged, warnings };
}

export function resolvePhaseProfile(config: PlannotatorConfig, phase: PhaseName): ResolvedPhaseProfile {
  const defaults = config.defaults ?? {};
  const phaseConfig = config.phases?.[phase] ?? {};

  return {
    model: resolveModel(defaults.model, phaseConfig.model),
    thinking: resolveThinking(defaults.thinking, phaseConfig.thinking),
    activeTools: resolveTools(defaults.activeTools, phaseConfig.activeTools),
    statusLabel: resolveString(defaults.statusLabel, phaseConfig.statusLabel),
    systemPrompt: resolveString(defaults.systemPrompt, phaseConfig.systemPrompt),
  };
}

function resolveModel(base: PhaseModelRef | null | undefined, override: PhaseModelRef | null | undefined): PhaseModelRef | undefined {
  if (override !== undefined) {
    return override ?? undefined;
  }
  return base ?? undefined;
}

function resolveThinking(base: ThinkingLevel | null | undefined, override: ThinkingLevel | null | undefined): ThinkingLevel | undefined {
  if (override !== undefined) {
    return override ?? undefined;
  }
  return base ?? undefined;
}

function resolveTools(base: string[] | null | undefined, override: string[] | null | undefined): string[] | undefined {
  if (override !== undefined) {
    if (override === null) return [];
    return [...override];
  }
  if (base === null) return [];
  return base ? [...base] : undefined;
}

function resolveString(base: string | null | undefined, override: string | null | undefined): string | undefined {
  if (override !== undefined) {
    if (override === null || override === "") return undefined;
    return override;
  }
  return base ?? undefined;
}

export function buildPromptVariables(options: {
  planFilePath: string;
  phase: RuntimePhase;
  totalCount: number;
  completedCount: number;
  remainingCount?: number;
  todoList?: string;
}): PromptVariables {
  const totalCount = options.totalCount;
  const completedCount = options.completedCount;
  const remainingCount = options.remainingCount ?? Math.max(totalCount - completedCount, 0);

  return {
    planFilePath: options.planFilePath,
    todoList: options.todoList ?? "",
    completedCount,
    totalCount,
    remainingCount,
    phase: options.phase,
  };
}

export function renderTemplate(template: string, vars: PromptVariables): PromptRenderResult {
  const unknownVariables = new Set<string>();
  const text = template.replace(/\$\{([a-zA-Z0-9_]+)\}/g, (_match, key: string) => {
    if (key in vars) {
      const value = vars[key as keyof PromptVariables];
      return value === undefined || value === null ? "" : String(value);
    }
    unknownVariables.add(key);
    return "";
  });

  return { text, unknownVariables: [...unknownVariables] };
}

export function formatTodoList(items: Array<{ step: number; text: string; completed: boolean }>): {
  todoList: string;
  completedCount: number;
  totalCount: number;
  remainingCount: number;
} {
  const totalCount = items.length;
  const completedCount = items.filter((item) => item.completed).length;
  const remainingItems = items.filter((item) => !item.completed);
  const todoList = remainingItems.length
    ? remainingItems.map((item) => `- [ ] ${item.step}. ${item.text}`).join("\n")
    : "";

  return {
    todoList,
    completedCount,
    totalCount,
    remainingCount: remainingItems.length,
  };
}
