import { readFile, writeFile, mkdir } from "node:fs/promises";
import { existsSync } from 'node:fs';
import { join, resolve } from 'node:path';

export type Criticality = 'low' | 'medium' | 'high';

export interface GuidelineContent {
  condition: string;
  action: string | null;
  description: string | null;
}

export interface Guideline {
  id: string;
  creationUtc: string;
  content: GuidelineContent;
  enabled: boolean;
  tags: string[];
  labels: string[];
  metadata: Record<string, unknown>;
  criticality: Criticality;
  title: string | null;
  priority: number;
}

export interface CreateGuidelineParams {
  condition: string;
  action?: string;
  description?: string;
  tags?: string[];
  labels?: string[];
  criticality?: Criticality;
  title?: string;
  priority?: number;
}

export interface UpdateGuidelineParams {
  condition?: string;
  action?: string | null;
  description?: string | null;
  enabled?: boolean;
  tags?: string[];
  labels?: string[];
  metadata?: Record<string, unknown>;
  criticality?: Criticality;
  title?: string | null;
  priority?: number;
}

export interface ListGuidelinesFilter {
  tags?: string[];
  labels?: string[];
}

interface GuidelineStoreData {
  version: string;
  guidelines: Guideline[];
  migrationLog: string[];
}

const GUIDELINES_REL = '.boo/guidelines';
const STORE_FILE = 'guidelines.json';
const CURRENT_VERSION = 'v0.11.0';

function storeDir(basePath?: string): string {
  return resolve(basePath ?? process.cwd(), GUIDELINES_REL);
}

function storePath(basePath?: string): string {
  return join(storeDir(basePath), STORE_FILE);
}

function tryParseJson<T>(raw: string): T | null {
  try {
    return JSON.parse(raw) as T;
  } catch {
    return null;
  }
}

let idCounter = 0;

function nextId(): string {
  idCounter++;
  return `gl_${Date.now()}_${idCounter}`;
}

function isoNow(): string {
  return new Date().toISOString();
}

async function ensureStoreDir(basePath?: string): Promise<void> {
  const dir = storeDir(basePath);
  if (!existsSync(dir)) {
    await mkdir(dir, { recursive: true });
  }
}

const MIGRATIONS: { from: string; to: string; migrate: (data: GuidelineStoreData) => GuidelineStoreData }[] = [
  {
    from: 'v0.1.0',
    to: 'v0.2.0',
    migrate: (data) => ({
      ...data,
      version: 'v0.2.0',
      guidelines: data.guidelines.map((g) => ({
        ...g,
        enabled: g.enabled ?? true,
      })),
      migrationLog: [...data.migrationLog, 'v0.1.0→v0.2.0: add enabled field'],
    }),
  },
  {
    from: 'v0.2.0',
    to: 'v0.3.0',
    migrate: (data) => ({
      ...data,
      version: 'v0.3.0',
      migrationLog: [...data.migrationLog, 'v0.2.0→v0.3.0: remove guideline_set'],
    }),
  },
  {
    from: 'v0.3.0',
    to: 'v0.4.0',
    migrate: (data) => ({
      ...data,
      version: 'v0.4.0',
      guidelines: data.guidelines.map((g) => ({
        ...g,
        content: {
          ...g.content,
          action: g.content.action ?? null,
          description: g.content.description ?? null,
        },
        metadata: g.metadata ?? {},
      })),
      migrationLog: [...data.migrationLog, 'v0.3.0→v0.4.0: add optional action, description, metadata'],
    }),
  },
  {
    from: 'v0.4.0',
    to: 'v0.5.0',
    migrate: (data) => ({
      ...data,
      version: 'v0.5.0',
      migrationLog: [...data.migrationLog, 'v0.4.0→v0.5.0: description as optional'],
    }),
  },
  {
    from: 'v0.5.0',
    to: 'v0.6.0',
    migrate: (data) => ({
      ...data,
      version: 'v0.6.0',
      guidelines: data.guidelines.map((g) => ({
        ...g,
        criticality: g.criticality ?? 'medium',
      })),
      migrationLog: [...data.migrationLog, 'v0.5.0→v0.6.0: add criticality'],
    }),
  },
  {
    from: 'v0.6.0',
    to: 'v0.7.0',
    migrate: (data) => ({
      ...data,
      version: 'v0.7.0',
      migrationLog: [...data.migrationLog, 'v0.6.0→v0.7.0: add composition_mode (optional)'],
    }),
  },
  {
    from: 'v0.7.0',
    to: 'v0.8.0',
    migrate: (data) => ({
      ...data,
      version: 'v0.8.0',
      migrationLog: [...data.migrationLog, 'v0.7.0→v0.8.0: add track (default true)'],
    }),
  },
  {
    from: 'v0.8.0',
    to: 'v0.9.0',
    migrate: (data) => ({
      ...data,
      version: 'v0.9.0',
      guidelines: data.guidelines.map((g) => ({
        ...g,
        labels: g.labels ?? [],
      })),
      migrationLog: [...data.migrationLog, 'v0.8.0→v0.9.0: add labels'],
    }),
  },
  {
    from: 'v0.9.0',
    to: 'v0.10.0',
    migrate: (data) => ({
      ...data,
      version: 'v0.10.0',
      guidelines: data.guidelines.map((g) => ({
        ...g,
        priority: g.priority ?? 0,
      })),
      migrationLog: [...data.migrationLog, 'v0.9.0→v0.10.0: add priority'],
    }),
  },
  {
    from: 'v0.10.0',
    to: 'v0.11.0',
    migrate: (data) => ({
      ...data,
      version: 'v0.11.0',
      guidelines: data.guidelines.map((g) => ({
        ...g,
        title: g.title ?? null,
      })),
      migrationLog: [...data.migrationLog, 'v0.10.0→v0.11.0: add title'],
    }),
  },
];

function applyMigrations(data: GuidelineStoreData): GuidelineStoreData {
  let current = { ...data };
  for (const migration of MIGRATIONS) {
    if (current.version === migration.from) {
      current = migration.migrate(current);
    }
  }
  return current;
}

async function readStore(basePath?: string): Promise<GuidelineStoreData> {
  try {
    const raw = await readFile(storePath(basePath), 'utf-8');
    const data = tryParseJson<GuidelineStoreData>(raw);
    if (!data) return { version: CURRENT_VERSION, guidelines: [], migrationLog: [] };
    if (data.version !== CURRENT_VERSION) {
      return applyMigrations(data);
    }
    return data;
  } catch {
    return { version: CURRENT_VERSION, guidelines: [], migrationLog: [] };
  }
}

async function writeStore(data: GuidelineStoreData, basePath?: string): Promise<void> {
  await ensureStoreDir(basePath);
  await writeFile(storePath(basePath), JSON.stringify(data, null, 2), 'utf-8');
}

export async function createGuideline(
  params: CreateGuidelineParams,
  basePath?: string,
): Promise<Guideline> {
  const data = await readStore(basePath);
  const guideline: Guideline = {
    id: nextId(),
    creationUtc: isoNow(),
    content: {
      condition: params.condition,
      action: params.action ?? null,
      description: params.description ?? null,
    },
    enabled: true,
    tags: params.tags ?? [],
    labels: params.labels ?? [],
    metadata: {},
    criticality: params.criticality ?? 'medium',
    title: params.title ?? null,
    priority: params.priority ?? 0,
  };
  data.guidelines.push(guideline);
  await writeStore(data, basePath);
  return guideline;
}

export async function listGuidelines(
  filter?: ListGuidelinesFilter,
  basePath?: string,
): Promise<Guideline[]> {
  const data = await readStore(basePath);
  let results = data.guidelines;

  if (filter?.tags && filter.tags.length > 0) {
    results = results.filter((g) => filter.tags!.some((tag) => g.tags.includes(tag)));
  }

  if (filter?.labels && filter.labels.length > 0) {
    results = results.filter((g) => filter.labels!.every((label) => g.labels.includes(label)));
  }

  return results;
}

export async function readGuideline(
  id: string,
  basePath?: string,
): Promise<Guideline | null> {
  const data = await readStore(basePath);
  return data.guidelines.find((g) => g.id === id) ?? null;
}

export async function updateGuideline(
  id: string,
  params: UpdateGuidelineParams,
  basePath?: string,
): Promise<Guideline | null> {
  const data = await readStore(basePath);
  const idx = data.guidelines.findIndex((g) => g.id === id);
  if (idx === -1) return null;

  const existing = data.guidelines[idx]!;

  if (params.condition !== undefined) existing.content.condition = params.condition;
  if (params.action !== undefined) existing.content.action = params.action;
  if (params.description !== undefined) existing.content.description = params.description;
  if (params.enabled !== undefined) existing.enabled = params.enabled;
  if (params.tags !== undefined) existing.tags = params.tags;
  if (params.labels !== undefined) existing.labels = params.labels;
  if (params.metadata !== undefined) existing.metadata = params.metadata;
  if (params.criticality !== undefined) existing.criticality = params.criticality;
  if (params.title !== undefined) existing.title = params.title;
  if (params.priority !== undefined) existing.priority = params.priority;

  data.guidelines[idx] = existing;
  await writeStore(data, basePath);
  return existing;
}

export async function deleteGuideline(
  id: string,
  basePath?: string,
): Promise<boolean> {
  const data = await readStore(basePath);
  const lenBefore = data.guidelines.length;
  data.guidelines = data.guidelines.filter((g) => g.id !== id);
  if (data.guidelines.length === lenBefore) return false;
  await writeStore(data, basePath);
  return true;
}

export async function findGuideline(
  content: { condition: string; action?: string },
  basePath?: string,
): Promise<Guideline | null> {
  const data = await readStore(basePath);
  return data.guidelines.find((g) => {
    const condMatch = g.content.condition === content.condition;
    if (!condMatch) return false;
    if (content.action !== undefined) {
      return g.content.action === content.action;
    }
    return true;
  }) ?? null;
}

export interface JourneyNode {
  id: string;
  action: string;
  description?: string;
}

export interface JourneyEdge {
  sourceNodeId: string;
  targetNodeId: string;
  condition: string;
}

export interface Journey {
  id: string;
  name: string;
  nodes: JourneyNode[];
  edges: JourneyEdge[];
}

export interface JourneyProjectionResult {
  guidelines: Guideline[];
  followUps: Map<string, string[]>;
}

/**
 * Project a Journey into an ordered list of Guidelines.
 * DFS traversal from root nodes: each (edge, node) pair → one Guideline.
 * Edge condition becomes guideline condition, node action becomes guideline action.
 * BFS queue avoids infinite loops via visited set.
 */
export function projectJourneyToGuidelines(
  journey: Journey,
  baseTags?: string[],
): JourneyProjectionResult {
  const guidelines: Guideline[] = [];
  const followUps = new Map<string, string[]>();
  const visited = new Set<string>();
  const nodeMap = new Map<string, JourneyNode>();

  for (const node of journey.nodes) {
    nodeMap.set(node.id, node);
  }

  const adjacency = new Map<string, JourneyEdge[]>();
  for (const edge of journey.edges) {
    const list = adjacency.get(edge.sourceNodeId) ?? [];
    list.push(edge);
    adjacency.set(edge.sourceNodeId, list);
  }

  // Find root nodes (no incoming edges)
  const hasIncoming = new Set<string>();
  for (const edge of journey.edges) {
    hasIncoming.add(edge.targetNodeId);
  }
  const roots = journey.nodes
    .filter((n) => !hasIncoming.has(n.id))
    .map((n) => n.id);

  const queue: { nodeId: string; fromEdge?: JourneyEdge }[] = [];

  // BFS from roots
  for (const rootId of roots) {
    if (!visited.has(rootId)) {
      queue.push({ nodeId: rootId });
    }
  }

  while (queue.length > 0) {
    const { nodeId, fromEdge } = queue.shift()!;
    if (visited.has(nodeId)) continue;
    visited.add(nodeId);

    const node = nodeMap.get(nodeId);
    if (!node) continue;

    // If we arrived via an edge, create a guideline
    if (fromEdge) {
      const guideline = createGuidelineFromJourneyEdge(
        journey,
        node,
        fromEdge,
        baseTags,
      );
      guidelines.push(guideline);

      // Track follow-ups
      const sourceId = findGuidelineForNode(fromEdge.sourceNodeId, journey.nodes);
      if (sourceId) {
        const existing = followUps.get(sourceId) ?? [];
        existing.push(guideline.id);
        followUps.set(sourceId, existing);
      }
    }

    // Enqueue downstream nodes
    const outgoingEdges = adjacency.get(nodeId) ?? [];
    for (const edge of outgoingEdges) {
      if (!visited.has(edge.targetNodeId)) {
        queue.push({ nodeId: edge.targetNodeId, fromEdge: edge });
      }
    }
  }

  return { guidelines, followUps };
}

function findGuidelineForNode(_nodeId: string, _nodes: JourneyNode[]): string | null {
  // Placeholder: in a full implementation, map nodeId → guideline id
  // For now return null — downstream consumers handle missing follow-ups gracefully
  return null;
}

function createGuidelineFromJourneyEdge(
  journey: Journey,
  targetNode: JourneyNode,
  edge: JourneyEdge,
  baseTags?: string[],
): Guideline {
  const now = isoNow();
  return {
    id: nextId(),
    creationUtc: now,
    content: {
      condition: edge.condition,
      action: targetNode.action,
      description: targetNode.description ?? null,
    },
    enabled: true,
    tags: baseTags ?? [journey.name],
    labels: [],
    metadata: {
      journey_id: journey.id,
      journey_node: targetNode.id,
      source_edge_id: `${edge.sourceNodeId}→${edge.targetNodeId}`,
    },
    criticality: 'medium',
    title: targetNode.description
      ? `[${journey.name}] ${targetNode.description.slice(0, 60)}`
      : null,
    priority: 0,
  };
}

export interface BacktrackCheckInput {
  journeyId: string;
  currentNodeId: string;
  previousNodeId: string;
}

export interface BacktrackCheckResult {
  journeyId: string;
  currentNodeId: string;
  previousNodeId: string;
  isBacktrack: boolean;
  recommendation: string | null;
}

/**
 * Check if moving from previousNodeId to currentNodeId is a backtrack
 * (regression to an already-visited node not on a forward path).
 */
export function checkBacktrack(
  input: BacktrackCheckInput,
  journey: Journey,
): BacktrackCheckResult {
  const adjacency = new Map<string, string[]>();
  for (const edge of journey.edges) {
    const list = adjacency.get(edge.sourceNodeId) ?? [];
    list.push(edge.targetNodeId);
    adjacency.set(edge.sourceNodeId, list);
  }

  // Find forward reachable nodes from the current node
  const forwardReachable = new Set<string>();
  const bfsQueue = [input.currentNodeId];
  while (bfsQueue.length > 0) {
    const nid = bfsQueue.shift()!;
    if (forwardReachable.has(nid)) continue;
    forwardReachable.add(nid);
    const next = adjacency.get(nid) ?? [];
    for (const n of next) {
      if (!forwardReachable.has(n)) bfsQueue.push(n);
    }
  }

  const isBacktrack = input.previousNodeId !== input.currentNodeId
    && !forwardReachable.has(input.previousNodeId)
    && input.previousNodeId !== input.currentNodeId;

  return {
    journeyId: input.journeyId,
    currentNodeId: input.currentNodeId,
    previousNodeId: input.previousNodeId,
    isBacktrack,
    recommendation: isBacktrack
      ? `Revisiting node "${input.previousNodeId}" after "${input.currentNodeId}" — this may indicate a regression. Consider whether the forward path from "${input.currentNodeId}" is the correct one.`
      : null,
  };
}
