// SimVSM JSON parser (Wertstrom P6, Baustein 1). Pure, synchronous, NEVER
// throws — every failure mode (not JSON, missing `alternatives[]`, a
// malformed node/link) becomes a structured result the preview can show the
// user, per §14.2's own "pre-import preview" requirement. Structural only:
// this layer keeps every raw node/link (including annotation-only classes
// like noteVSM/customText) — the MAPPING decision (which classes become a
// real VsmNode) is mapping-registry.ts + node-mapper.ts's job, not this
// file's. Real-corpus validated: parses all 25/25 real corpus files without
// throwing (internal/__tests__/parser.real-corpus.test.ts, env-gated).

import type {
  SimVsmParameter,
  SimVsmParsedAlternative,
  SimVsmParsedLink,
  SimVsmParsedNode,
  SimVsmParseIssue,
  SimVsmParseResult,
  SimVsmRawAlternative,
  SimVsmRawFile,
} from './types'

export const PARSER_VERSION = 'simvsm-parser-1'

/** Real corpus max is ~13.4 MiB (see README.md "Größenlimit / Body-Limit" —
 * the largest real file is well above the execution-prompt's ~6 MB
 * assumption). This constant is a sanity ceiling for the PURE parser only
 * (protects a caller from ever handing it something absurd, e.g. a
 * mis-selected multi-GB file) — the much stricter, deployment-driven per-file
 * HTTP upload limit lives in the route (app/api/wertstrom/import/preview),
 * not here. Deliberately generous so the real-corpus test can exercise every
 * file through this function regardless of the route's own limit. */
export const PARSER_MAX_JSON_BYTES = 64 * 1024 * 1024

function safeJsonParse(rawText: string, fileName: string): { ok: true; value: unknown } | { ok: false; error: string } {
  if (Buffer.byteLength(rawText, 'utf8') > PARSER_MAX_JSON_BYTES) {
    return { ok: false, error: `Datei überschreitet die Parser-Obergrenze (${Math.round(PARSER_MAX_JSON_BYTES / 1024 / 1024)} MB).` }
  }
  try {
    return { ok: true, value: JSON.parse(rawText) }
  } catch (e) {
    return { ok: false, error: `"${fileName}" ist kein gültiges JSON: ${e instanceof Error ? e.message : String(e)}` }
  }
}

function parseParameters(raw: SimVsmParameter[] | undefined): Map<string, SimVsmParameter> {
  const params = new Map<string, SimVsmParameter>()
  if (!Array.isArray(raw)) return params
  for (const p of raw) {
    if (p && typeof p === 'object' && typeof p.class === 'string') {
      params.set(p.class, p)
    }
  }
  return params
}

function parseAlternative(alt: SimVsmRawAlternative, index: number, issues: SimVsmParseIssue[]): SimVsmParsedAlternative {
  const rawNodes = Array.isArray(alt.model?.nodeDataArray) ? alt.model!.nodeDataArray! : []
  const rawLinks = Array.isArray(alt.model?.linkDataArray) ? alt.model!.linkDataArray! : []

  const nodes: SimVsmParsedNode[] = []
  for (const raw of rawNodes) {
    if (!raw || raw.key === undefined || raw.key === null) {
      issues.push({ code: 'node_missing_key', message: `Alternative ${index + 1}: Knoten ohne "key" übersprungen.` })
      continue
    }
    const nameSource = typeof raw.nodeName === 'string' && raw.nodeName.trim() ? raw.nodeName : raw.text
    nodes.push({
      key: String(raw.key),
      category: typeof raw.category === 'string' && raw.category.length > 0 ? raw.category : 'item',
      simvsmClass: typeof raw.class === 'string' && raw.class.length > 0 ? raw.class : 'unknown',
      nodeName: typeof nameSource === 'string' ? nameSource.trim() : '',
      loc: typeof raw.loc === 'string' ? raw.loc : undefined,
      text: typeof raw.text === 'string' ? raw.text : undefined,
      params: parseParameters(raw.parameters),
    })
  }

  const nodeKeys = new Set(nodes.map((n) => n.key))
  const links: SimVsmParsedLink[] = []
  for (const raw of rawLinks) {
    if (!raw || raw.from === undefined || raw.from === null || raw.to === undefined || raw.to === null) {
      issues.push({ code: 'link_missing_endpoint', message: `Alternative ${index + 1}: Verbindung ohne "from"/"to" übersprungen.` })
      continue
    }
    const fromKey = String(raw.from)
    const toKey = String(raw.to)
    if (!nodeKeys.has(fromKey) || !nodeKeys.has(toKey)) {
      issues.push({ code: 'link_dangling_endpoint', message: `Alternative ${index + 1}: Verbindung verweist auf einen unbekannten Knoten, übersprungen.` })
      continue
    }
    links.push({
      key: raw.key !== undefined && raw.key !== null ? String(raw.key) : `${fromKey}->${toKey}`,
      simvsmClass: typeof raw.class === 'string' && raw.class.length > 0 ? raw.class : 'unknown',
      fromKey,
      toKey,
      text: typeof raw.text === 'string' && raw.text.trim() ? raw.text.trim() : undefined,
    })
  }

  return {
    index,
    name: typeof alt.name === 'string' && alt.name.trim() ? alt.name.trim() : `Alternative ${index + 1}`,
    isMainFlag: alt.isMain === true,
    nodes,
    links,
    hasResultData: Array.isArray(alt.resultData) && alt.resultData.length > 0,
    modificationTime: typeof alt.modificationTime === 'string' && alt.modificationTime.trim() ? alt.modificationTime : undefined,
  }
}

/** Parses an already-decoded JS value (call `parseSimVsmJsonText` for raw
 * file bytes). Exported separately so tests/preview code that already has a
 * parsed object (e.g. re-used across preview+confirm) never pays for a
 * second JSON.parse. Never throws — a pathological input (e.g. a getter
 * that throws, reachable in principle by anything calling this directly
 * with a non-`JSON.parse` value) funnels into `{ok:false}` just like
 * `parseSimVsmJsonText`'s own outer guard. */
export function parseSimVsmValue(raw: unknown, fileName: string): SimVsmParseResult {
  try {
    return parseSimVsmValueUnsafe(raw, fileName)
  } catch (e) {
    return { ok: false, fileName, error: `"${fileName}" konnte nicht gelesen werden: ${e instanceof Error ? e.message : String(e)}` }
  }
}

function parseSimVsmValueUnsafe(raw: unknown, fileName: string): SimVsmParseResult {
  if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
    return { ok: false, fileName, error: `"${fileName}" ist kein SimVSM-Dokument (kein JSON-Objekt auf oberster Ebene).` }
  }
  const file = raw as SimVsmRawFile
  if (!Array.isArray(file.alternatives) || file.alternatives.length === 0) {
    return { ok: false, fileName, error: `"${fileName}" enthält kein "alternatives"-Array — kein SimVSM-Wertstrom.` }
  }

  const issues: SimVsmParseIssue[] = []
  const alternatives = file.alternatives.map((alt, index) => parseAlternative(alt ?? {}, index, issues))

  return {
    ok: true,
    fileName,
    modelName: typeof file.name === 'string' && file.name.trim() ? file.name.trim() : fileName.replace(/\.json$/i, ''),
    description: typeof file.description === 'string' ? file.description : '',
    createdWithVersion: typeof file.createdWithVersion === 'string' ? file.createdWithVersion : null,
    mainVersion: typeof file.mainVersion === 'string' ? file.mainVersion : null,
    productCount: Array.isArray(file.settings?.products) ? file.settings!.products!.length : 0,
    shiftCalendarCount: Array.isArray(file.settings?.shiftCalendars) ? file.settings!.shiftCalendars!.length : 0,
    alternatives,
    issues,
  }
}

/** Parses raw file text end-to-end (JSON.parse + structural parse). Never
 * throws — both stages funnel into the `{ok:false}` branch (`parseSimVsmValue`
 * is itself defensive, see its own doc comment). */
export function parseSimVsmJsonText(rawText: string, fileName: string): SimVsmParseResult {
  const parsed = safeJsonParse(rawText, fileName)
  if (!parsed.ok) return { ok: false, fileName, error: parsed.error }
  return parseSimVsmValue(parsed.value, fileName)
}
