import { z, guardInput, type Issue, type ValidationResult } from '../shared/validation';

export const PROCESS_LIMITS = Object.freeze({ nodes: 100, edges: 200, sourceBytes: 30_000, jsonBytes: 150_000, svgBytes: 5_000_000, label: 160, description: 400 });
const id = z.string().regex(/^[A-Za-z][A-Za-z0-9_]{0,47}$/, 'Use a letter followed by letters, numbers or underscores.');
const label = z.string().trim().min(1, 'Enter a name for this step.').max(PROCESS_LIMITS.label);
export const nodeSchema = z.strictObject({ id, type: z.enum(['start', 'end', 'process', 'decision', 'system', 'store']), label, team: z.string().max(100), description: z.string().max(PROCESS_LIMITS.description) });
export const edgeSchema = z.strictObject({ id, from: id, to: id, label: z.string().max(80) });
export const graphSchema = z.strictObject({ title: label, direction: z.enum(['TB', 'LR']), nodes: z.array(nodeSchema).min(1).max(PROCESS_LIMITS.nodes), edges: z.array(edgeSchema).max(PROCESS_LIMITS.edges) });
export const processSchema = z.discriminatedUnion('mode', [
  z.strictObject({ schemaVersion: z.literal(1), tool: z.literal('process-mapper'), mode: z.literal('guided'), graph: graphSchema }),
  z.strictObject({ schemaVersion: z.literal(1), tool: z.literal('process-mapper'), mode: z.literal('source'), source: z.string().max(PROCESS_LIMITS.sourceBytes), preservedGraph: graphSchema.nullable() }),
]);
export type GuidedGraph = z.infer<typeof graphSchema>;
export type ProcessState = z.infer<typeof processSchema>;
export type ProcessNode = z.infer<typeof nodeSchema>;
export type ProcessEdge = z.infer<typeof edgeSchema>;

/** Node IDs are syntax; labels are always quoted Mermaid text, with delimiters encoded once. */
export function encodeLabel(value: string): string {
  return value.replace(/[\x00-\x1f\x7f"\[\]{}()<>`|;#&\\]/g, char => `#${char.codePointAt(0)};`);
}

export function compileGuided(graph: GuidedGraph): string {
  const lines = [`flowchart ${graph.direction}`];
  for (const node of graph.nodes) {
    const text = encodeLabel(node.label);
    const shape = node.type === 'start' || node.type === 'end' ? `(["${text}"])`
      : node.type === 'decision' ? `{"${text}"}`
      : node.type === 'store' ? `[("${text}")]`
      : node.type === 'system' ? `[["${text}"]]` : `["${text}"]`;
    lines.push(`  ${node.id}${shape}`);
  }
  for (const edge of graph.edges) lines.push(`  ${edge.from} -->${edge.label ? `|"${encodeLabel(edge.label)}"|` : ''} ${edge.to}`);
  return lines.join('\n');
}

function inspectGraph(graph: GuidedGraph, prefix: string): { errors: Issue[]; warnings: Issue[] } {
  const errors: Issue[] = [], warnings: Issue[] = [];
  const nodes = new Set<string>(), edges = new Set<string>(), connected = new Set<string>();
  graph.nodes.forEach((node, index) => {
    if (nodes.has(node.id)) errors.push({ path: `${prefix}.nodes.${index}.id`, message: 'Each step needs a different ID.' });
    nodes.add(node.id);
  });
  graph.edges.forEach((edge, index) => {
    if (edges.has(edge.id)) errors.push({ path: `${prefix}.edges.${index}.id`, message: 'Each connection needs a different ID.' });
    edges.add(edge.id);
    if (!nodes.has(edge.from) || !nodes.has(edge.to)) errors.push({ path: `${prefix}.edges.${index}`, message: 'Choose existing steps at both ends of this connection.' });
    connected.add(edge.from); connected.add(edge.to);
  });
  if (graph.nodes.length > 1) graph.nodes.forEach((node, index) => {
    if (!connected.has(node.id)) warnings.push({ path: `${prefix}.nodes.${index}`, message: `${node.label} has no connections. Check whether it belongs in this process.` });
  });
  if (graph.nodes.length > 1) {
    const reached = new Set([graph.nodes[0].id]);
    let changed = true;
    while (changed) {
      changed = false;
      for (const edge of graph.edges) {
        if (reached.has(edge.from) && !reached.has(edge.to)) { reached.add(edge.to); changed = true; }
        if (reached.has(edge.to) && !reached.has(edge.from)) { reached.add(edge.from); changed = true; }
      }
    }
    if (graph.nodes.some(node => !reached.has(node.id))) warnings.push({ path: `${prefix}.edges`, message: 'This map has separate groups of steps. Check whether they should be connected.' });
  }
  // Process loops are legitimate. No dependency-cycle rule from the project planner applies here.
  return { errors, warnings };
}

const tokenId = '[A-Za-z][A-Za-z0-9_]{0,47}';
const quoted = '"[^"\\r\\n]*"';
const shape = `(?:\\[\\(${quoted}\\)\\]|\\[\\[${quoted}\\]\\]|\\(\\[${quoted}\\]\\)|\\[${quoted}\\]|\\{${quoted}\\})`;
const nodeToken = `${tokenId}(?:${shape})?`;
const nodeLine = new RegExp(`^(${tokenId})${shape}$`, 'u');
const edgeLine = new RegExp(`^(${nodeToken})\\s*(-->|-\\.->|==>)(?:\\|(${quoted})\\|)?\\s*(${nodeToken})$`, 'u');

/** A deliberately small source grammar: no directives, configuration, CSS, actions or resources. */
export function validateSource(source: string): Issue[] {
  if (new TextEncoder().encode(source).length > PROCESS_LIMITS.sourceBytes) return [{ path: 'source', message: 'Keep Mermaid source below 30 KB.' }];
  if (!source.trim()) return [{ path: 'source', message: 'Enter a flowchart or sequence diagram.' }];
  if (/%%\s*\{|^\s*---|<\/?[A-Za-z!]|\b(?:javascript|vbscript|data|https?|file):|@\{|:::|\$\$/imu.test(source)) return [{ path: 'source', message: 'Configuration, HTML, links, images, icons and custom styling are not supported.' }];
  const lines = source.replace(/\r\n?/g, '\n').split('\n').map((value, index) => ({ value: value.trim(), index })).filter(line => line.value && !line.value.startsWith('%%'));
  const first = lines.shift()?.value;
  const flow = /^(?:flowchart|graph)\s+(?:TB|TD|LR|RL|BT)$/.test(first ?? '');
  const sequence = first === 'sequenceDiagram';
  if (!flow && !sequence) return [{ path: 'source', message: 'Start with “flowchart TB”, “flowchart LR” or “sequenceDiagram”. Other diagram types are not supported.' }];
  const errors: Issue[] = [], ids = new Set<string>();
  let edges = 0, depth = 0;
  if (lines.length > 420) return [{ path: 'source', message: 'Use no more than 420 source statements.' }];
  for (const { value, index } of lines) {
    let accepted = false;
    if (flow) {
      const node = value.match(nodeLine), edge = value.match(edgeLine);
      if (node) { ids.add(node[1]); accepted = true; }
      if (edge) { ids.add(edge[1].match(new RegExp(`^${tokenId}`))![0]); ids.add(edge[4].match(new RegExp(`^${tokenId}`))![0]); edges++; accepted = true; }
    } else {
      const actor = value.match(new RegExp(`^(?:participant|actor)\\s+(${tokenId})(?:\\s+as\\s+([^<>\\r\\n{}]+))?$`, 'u'));
      const message = value.match(new RegExp(`^(${tokenId})\\s*(?:-->>|->>|-->|->|--x|-x|--\\)|-\\))\\s*(${tokenId})\\s*:\\s*([^<>\\r\\n{}]+)$`, 'u'));
      const note = value.match(new RegExp(`^Note\\s+(?:(?:right|left)\\s+of\\s+(${tokenId})|over\\s+(${tokenId})(?:\\s*,\\s*(${tokenId}))?)\\s*:\\s*([^<>\\r\\n{}]+)$`, 'u'));
      const active = value.match(new RegExp(`^(?:activate|deactivate)\\s+(${tokenId})$`));
      if (actor) { ids.add(actor[1]); accepted = true; }
      if (message) { ids.add(message[1]); ids.add(message[2]); edges++; accepted = true; }
      if (note) { [note[1], note[2], note[3]].filter(Boolean).forEach(id => ids.add(id)); accepted = true; }
      if (active) { ids.add(active[1]); accepted = true; }
      if (/^(?:alt|opt|loop|par)\s+[^<>{}\r\n]+$/u.test(value)) { depth++; accepted = depth <= 8; }
      if (/^(?:else|and)(?:\s+[^<>{}\r\n]+)?$/u.test(value) && depth > 0) accepted = true;
      if (value === 'end' && depth > 0) { depth--; accepted = true; }
      if (value === 'autonumber') accepted = true;
    }
    if (!accepted) errors.push({ path: `source.line.${index + 1}`, message: `Line ${index + 1} is outside the supported syntax. Use quoted flowchart labels or simple sequence messages; see the source guide.` });
    if (errors.length >= 8) break;
  }
  if (depth) errors.push({ path: 'source', message: 'Close each sequence block with “end”.' });
  if (!ids.size) errors.push({ path: 'source', message: 'Add at least one step or participant.' });
  if (ids.size > PROCESS_LIMITS.nodes || edges > PROCESS_LIMITS.edges) errors.push({ path: 'source', message: 'Use at most 100 steps/participants and 200 connections/messages.' });
  return errors;
}

export function validateProcess(input: unknown): ValidationResult<ProcessState> {
  try { guardInput(input, PROCESS_LIMITS.jsonBytes); }
  catch { return { ok: false, errors: [{ path: '', message: 'This file is too large or contains unsupported objects, unsafe keys or too many nested levels.' }], warnings: [] }; }
  const result = processSchema.safeParse(input);
  if (!result.success) return { ok: false, errors: result.error.issues.map(issue => ({ path: issue.path.join('.'), message: issue.message })), warnings: [] };
  const state = result.data, graph = state.mode === 'guided' ? state.graph : state.preservedGraph;
  const issues = graph ? inspectGraph(graph, state.mode === 'guided' ? 'graph' : 'preservedGraph') : { errors: [], warnings: [] };
  const source = state.mode === 'guided' ? compileGuided(state.graph) : state.source;
  issues.errors.push(...validateSource(source));
  return issues.errors.length ? { ok: false, ...issues } : { ok: true, value: state, warnings: issues.warnings };
}

export function parseProcessJson(text: string): ValidationResult<ProcessState> {
  if (new TextEncoder().encode(text).length > PROCESS_LIMITS.jsonBytes) return { ok: false, errors: [{ path: '', message: 'Choose a process JSON file smaller than 150 KB.' }], warnings: [] };
  try { return validateProcess(JSON.parse(text)); }
  catch { return { ok: false, errors: [{ path: '', message: 'This is not valid JSON. Your current process has been kept.' }], warnings: [] }; }
}

export function toSourceMode(state: ProcessState): ProcessState {
  return state.mode === 'source' ? structuredClone(state) : { schemaVersion: 1, tool: 'process-mapper', mode: 'source', source: compileGuided(state.graph), preservedGraph: structuredClone(state.graph) };
}
export function restoreGuided(state: ProcessState): ProcessState | null {
  if (state.mode === 'guided') return structuredClone(state);
  return state.preservedGraph ? { schemaVersion: 1, tool: 'process-mapper', mode: 'guided', graph: structuredClone(state.preservedGraph) } : null;
}
export function nextId(prefix: 'step' | 'link', existing: string[]): string {
  const used = new Set(existing); let index = 1;
  while (used.has(`${prefix}_${index}`)) index++;
  return `${prefix}_${index}`;
}

const node = (id: string, type: ProcessNode['type'], label: string, team = '', description = ''): ProcessNode => ({ id, type, label, team, description });
const edge = (id: string, from: string, to: string, label = ''): ProcessEdge => ({ id, from, to, label });
const example = (title: string, nodes: ProcessNode[], edges: ProcessEdge[], direction: GuidedGraph['direction'] = 'TB'): Extract<ProcessState, { mode: 'guided' }> => ({ schemaVersion: 1, tool: 'process-mapper', mode: 'guided', graph: { title, nodes, edges, direction } });
export const processExamples = {
  onboarding: example('Customer onboarding', [node('enquiry', 'start', 'New customer enquiry', 'Sales'), node('details', 'process', 'Confirm needs and details', 'Sales'), node('approved', 'decision', 'Ready to get started?', 'Customer'), node('account', 'system', 'Set up customer account', 'Operations'), node('welcome', 'end', 'Send welcome and next steps', 'Support')], [edge('e1', 'enquiry', 'details'), edge('e2', 'details', 'approved'), edge('e3', 'approved', 'account', 'Yes'), edge('e4', 'approved', 'details', 'Need more information'), edge('e5', 'account', 'welcome')]),
  approval: example('Purchase approval', [node('request', 'start', 'Team submits purchase request', 'Team'), node('budget', 'decision', 'Within approved budget?', 'Manager'), node('review', 'process', 'Review exception', 'Finance'), node('order', 'system', 'Create purchase order', 'Purchasing'), node('received', 'end', 'Confirm delivery', 'Team')], [edge('e1', 'request', 'budget'), edge('e2', 'budget', 'order', 'Yes'), edge('e3', 'budget', 'review', 'No'), edge('e4', 'review', 'budget', 'Revised request'), edge('e5', 'order', 'received')]),
  service: example('Service-request handling', [node('ticket', 'start', 'Customer reports an issue', 'Customer'), node('triage', 'process', 'Review and assign request', 'Support'), node('fix', 'process', 'Investigate and resolve', 'Service team'), node('check', 'decision', 'Problem resolved?', 'Customer'), node('close', 'end', 'Close request and record solution', 'Support')], [edge('e1', 'ticket', 'triage'), edge('e2', 'triage', 'fix'), edge('e3', 'fix', 'check'), edge('e4', 'check', 'fix', 'No'), edge('e5', 'check', 'close', 'Yes')]),
  systems: example('Website, CRM and accounting', [node('web', 'system', 'Company website', 'Marketing'), node('crm', 'store', 'Customer records in CRM', 'Sales'), node('review', 'process', 'Approve customer order', 'Operations'), node('accounts', 'system', 'Accounting software', 'Finance'), node('receipt', 'end', 'Customer receives confirmation', 'Customer')], [edge('e1', 'web', 'crm', 'Enquiry details'), edge('e2', 'crm', 'review', 'Order details'), edge('e3', 'review', 'accounts', 'Approved order'), edge('e4', 'accounts', 'receipt', 'Invoice or receipt')], 'LR'),
} satisfies Record<string, ProcessState>;
