import { randomUUID } from 'node:crypto'
import { readFile, stat } from 'node:fs/promises'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import type { CaseDocument, CaseState, ClaimExhibit, DocumentCaptureKind, NoteExhibit, NotePresentation, PartyKind, SourceFileType } from '../src/types.js'

type MysteryDocument = {
  key: string
  title: string
  fileType: SourceFileType
  captureKind?: DocumentCaptureKind
  publishedAt: string
  body?: string[]
  metadata?: Record<string, string>
  asset?: string
  requiredFlags?: string[]
}
type MysteryGraph = {
  entry: string
  nodes: { key: string; type: 'cutscene' | 'dialogue' | 'level' | 'det_gate' | 'llm_gate' | 'merit' | 'phone'; label?: string; x: number; y: number
    componentKey?: string; templateSlug?: string; version?: number; awardsFlag?: string
    terminals?: { key: string; label?: string; to?: string | null; npc?: string }[]
    utterances?: { key?: string; parent?: string; terminal?: string; npc?: string; pose?: string; text: string; utterer?: 'npc' | 'player'; awardsFlag?: string; requiresFlag?: string }[] }[]
}
type MysteryNarrative = {
  cast: { key: string; name: string; role?: string; defaultPose?: string; phoneNumber?: string; email?: string; poses?: { poseKey: string; assetId: string }[] }[]
  graph?: MysteryGraph
}
type MysteryGoal = {
  key:string; title:string; instructions?:string; completionMessage?:string; enabled?:boolean; requiredFlags:string[]
}
type MysteryEvidenceMatchRule = {
  name:string; sourceLabel?:string; sourceUri?:string; flagKey:string; minimumAnchorMatches?:number; enabled?:boolean
  anchors:{ phrase:string; minimumSimilarity?:number }[]
}
type MysterySemanticRule = {
  goalKey:string; name:string; targetSubject:string; relatedSubject?:string; assertion:string; successFlagKey:string
  relatedFlagKey?:string; minimumConfidence?:number; evaluatorVersion?:string; enabled?:boolean
}
// A single playable board (level template). Assets are filenames resolved against
// the mystery folder (e.g. "assets/photo.png"), matching the on-disk layout.
type MysteryLevel = {
  slug: string
  name: string
  title: string
  subtitle?: string
  timelineRange?: { start: string; end: string }
  brief?: { body: string; concepts?: { label: string; context: string; expectedPartyKind: PartyKind }[]; checklist?: string[] }
  documents?: MysteryDocument[]
  folders?: { key: string; title: string; content: string; x: number; y: number; width: number; members: string[] }[]
  claims?: { key:string;statement:string;x:number;y:number;width?:number;height?:number }[]
  notes?: { title?:string;content:string;presentation?:NotePresentation;x:number;y:number;width?:number;height?:number }[]
  report?: { title?:string;requiredForCompletion?:boolean }
  goals?: MysteryGoal[]
  evidenceMatchRules?: MysteryEvidenceMatchRule[]
  evidenceSemanticRules?: MysterySemanticRule[]
}
// Legacy single-manifest: one level plus an optional narrative in the same file.
type MysteryManifest = MysteryLevel & { narrative?: MysteryNarrative }
// New self-contained format: a mystery with every level it uses embedded.
type MysteryFile = { slug: string; title: string; levels: MysteryLevel[]; narrative?: MysteryNarrative }

function requireOk(response: Response, action: string) {
  if (response.ok) return response
  return response.text().then(body => { throw new Error(`${action} failed (${response.status}): ${body}`) })
}

const MIME_BY_EXT: Record<string, string> = {
  png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', gif: 'image/gif', webp: 'image/webp', svg: 'image/svg+xml',
  pdf: 'application/pdf', mp3: 'audio/mpeg', wav: 'audio/wav', ogg: 'audio/ogg', m4a: 'audio/mp4', txt: 'text/plain',
}
function mimeFor(filename: string) { return MIME_BY_EXT[filename.split('.').pop()?.toLowerCase() || ''] || 'application/octet-stream' }

async function uploadAsset(baseUrl: string, levelId: string, manifestDir: string, document: MysteryDocument, authorization?: string) {
  if (!document.asset) return undefined
  const assetPath = path.resolve(manifestDir, document.asset)
  const filename = path.basename(assetPath)
  const form = new FormData()
  form.append('file', new Blob([await readFile(assetPath)], { type: mimeFor(filename) }), filename)
  const response = await requireOk(await fetch(`${baseUrl}/api/levels/${levelId}/documents?edit=1`, { method: 'POST', headers: authorization ? { authorization } : undefined, body: form }), `Upload ${document.asset}`)
  return await response.json() as CaseDocument
}

type ImportHeaders = Record<string, string>
function authHeaders(adminJwt?: string): { authorization?: string; headers: ImportHeaders } {
  const authorization = adminJwt ? `Bearer ${adminJwt}` : undefined
  return { authorization, headers: { 'content-type': 'application/json', ...(authorization ? { authorization } : {}) } }
}

type LevelResult = { template: { slug: string; currentVersion: number }; authoringLevelId: string; playableLevel: CaseState }

// Import one board: create a mutable authoring level, upload assets, save exhibits,
// goals and evidence rules, freeze an immutable template, and instantiate a playable copy.
async function importLevel(baseUrl: string, folderDir: string, level: MysteryLevel, headers: ImportHeaders, authorization?: string): Promise<LevelResult> {
  const authoringId = `${level.slug}-authoring-${Date.now()}`
  const createdResponse = await requireOk(await fetch(`${baseUrl}/api/levels`, {
    method: 'POST', headers,
    body: JSON.stringify({ id: authoringId, title: level.title, subtitle: level.subtitle || '' }),
  }), 'Create authoring level')
  const state = await createdResponse.json() as CaseState

  const documentPositions = new Map<string, { x: number; y: number }>()
  const levelFolders = level.folders || []
  levelFolders.forEach((folder, folderIndex) => folder.members.forEach((key, memberIndex) => documentPositions.set(key, { x: folder.x + 70 + memberIndex * 205, y: folder.y + 230 + folderIndex * 35 })))
  const documents = new Map<string, CaseDocument>()
  for (const source of level.documents || []) {
    const uploaded = await uploadAsset(baseUrl, state.id, folderDir, source, authorization)
    documents.set(source.key, {
      id: uploaded?.id || randomUUID(), type: 'document', title: source.title, publishedAt: source.publishedAt,
      x: documentPositions.get(source.key)?.x || 100, y: documentPositions.get(source.key)?.y || 100,
      width: uploaded?.width || 174, height: uploaded?.height || 145, rotation: 0, zIndex: uploaded?.zIndex || 1, hidden: false,
      body: source.body || [], regions: [], assetId: uploaded?.assetId,
      fileName: uploaded?.fileName, mimeType: uploaded?.mimeType, fileSize: uploaded?.fileSize,
      fileType: source.fileType, captureKind:source.captureKind || 'unclassified', metadata: source.metadata || {}, requiredFlags: source.requiredFlags || [],
    })
  }

  const folderIds = new Map(levelFolders.map(folder => [folder.key, randomUUID()]))
  if (level.brief) state.brief = { body: level.brief.body, concepts: (level.brief.concepts || []).map(concept => ({ id: randomUUID(), ...concept })), checklist: (level.brief.checklist || []).map(text => ({ id: randomUUID(), text })) }
  state.views = state.views.map(view => view.type === 'timeline' ? { ...view, rangeMode: level.timelineRange ? 'fixed' : 'auto', range: level.timelineRange } : view)
  const folders = levelFolders.map(folder => ({
    id: folderIds.get(folder.key)!, type: 'folder', title: folder.title, content: folder.content,
    x: folder.x, y: folder.y, width: folder.width, height: 166, rotation: 0, zIndex: 1, hidden: false, isOpen: false,
  } as const))
  const claims:ClaimExhibit[]=(level.claims || []).map(claim => ({ id:randomUUID(),type:'claim',title:claim.statement,statement:claim.statement,
    x:claim.x,y:claim.y,width:claim.width || 310,height:claim.height || 180,rotation:0,zIndex:2,hidden:false }))
  const notes:NoteExhibit[]=(level.notes || []).map(note => ({ id:randomUUID(),type:'note',title:note.title || 'NOTE',content:note.content,
    presentation:note.presentation || 'luggage',x:note.x,y:note.y,width:note.width || 230,height:note.height || 180,rotation:0,zIndex:2,hidden:false }))
  state.exhibits = [...documents.values(), ...folders,...claims,...notes]
  state.relations = levelFolders.flatMap(folder => folder.members.map((key, memberIndex) => {
    const document = documents.get(key)
    if (!document) throw new Error(`Folder ${folder.key} refers to unknown document ${key}`)
    return {
      id: `contains:${folderIds.get(folder.key)}:${document.id}`,
      fromExhibitId: folderIds.get(folder.key)!, toExhibitId: document.id, type: 'contains' as const, sortOrder: memberIndex,
    }
  }))
  state.connections = []
  state.report=level.report ? { title:level.report.title || 'Case Report',investigatorName:'',requiredForCompletion:level.report.requiredForCompletion !== false,
    status:'draft',issues:[],claims:[] } : undefined
  state.viewport = { x: 0, y: 28, zoom: 0.7 }

  await requireOk(await fetch(`${baseUrl}/api/levels/${state.id}?edit=1`, {
    method: 'PUT', headers, body: JSON.stringify(state),
  }), 'Save authored level')

  const goalIds = new Map<string,string>()
  for (const goal of level.goals || []) {
    const response = await requireOk(await fetch(`${baseUrl}/api/levels/${state.id}/goals`, {
      method:'POST',headers,body:JSON.stringify(goal),
    }), `Create goal ${goal.key}`)
    const created = await response.json() as { id:string;key:string }
    goalIds.set(created.key,created.id)
  }
  for (const rule of level.evidenceMatchRules || []) await requireOk(await fetch(
    `${baseUrl}/api/levels/${state.id}/evidence-match-rules`, { method:'POST',headers,body:JSON.stringify(rule) }),
  `Create evidence match rule ${rule.name}`)
  for (const rule of level.evidenceSemanticRules || []) {
    const goalId = goalIds.get(rule.goalKey)
    if (!goalId) throw new Error(`Semantic evidence rule ${rule.name} refers to unknown goal ${rule.goalKey}`)
    await requireOk(await fetch(`${baseUrl}/api/levels/${state.id}/evidence-semantic-rules`, {
      method:'POST',headers,body:JSON.stringify({ ...rule,goalKey:undefined,goalId }),
    }), `Create semantic evidence rule ${rule.name}`)
  }
  const templateResponse = await requireOk(await fetch(`${baseUrl}/api/levels/${state.id}/templates?edit=1`, {
    method: 'POST', headers, body: JSON.stringify({ slug: level.slug, name: level.name || level.title }),
  }), 'Freeze level template')
  const template = await templateResponse.json() as { slug: string; currentVersion: number }

  const playableId = `${level.slug}-case-${Date.now()}`
  const playableResponse = await requireOk(await fetch(`${baseUrl}/api/templates/${level.slug}/levels?edit=1`, {
    method: 'POST', headers, body: JSON.stringify({ id: playableId, title: level.title }),
  }), 'Instantiate playable level')
  const playable = await playableResponse.json() as CaseState
  return { template, authoringLevelId: state.id, playableLevel: playable }
}

// Author the narrative mystery (NPC cast) and seed its story flow graph.
async function importNarrative(baseUrl: string, slug: string, title: string, narrative: MysteryNarrative, headers: ImportHeaders, authorization?: string): Promise<{ slug: string }> {
  const mysteryResponse = await requireOk(await fetch(`${baseUrl}/api/mysteries?edit=1`, {
    method: 'POST', headers, body: JSON.stringify({ slug, title, cast: narrative.cast }),
  }), 'Author narrative mystery')
  const mystery = await mysteryResponse.json() as { slug: string }
  if (narrative.graph) {
    const listResponse = await requireOk(await fetch(`${baseUrl}/api/admin/mysteries`, { headers: authorization ? { authorization } : undefined }), 'List mysteries')
    const mysteries = await listResponse.json() as { id: string; slug: string }[]
    const mysteryId = mysteries.find(m => m.slug === slug)?.id
    if (mysteryId) await requireOk(await fetch(`${baseUrl}/api/admin/mysteries/${mysteryId}/graph`, {
      method: 'POST', headers, body: JSON.stringify(narrative.graph),
    }), 'Seed story graph')
  }
  return mystery
}

// Legacy single-manifest import: one level plus an optional narrative in the same file.
export async function importMysteryTemplate(manifestPath: string, baseUrl = 'http://localhost:8787', adminJwt = process.env.OSINT_ADMIN_JWT) {
  const absoluteManifest = path.resolve(manifestPath)
  const manifest = JSON.parse(await readFile(absoluteManifest, 'utf8')) as MysteryManifest
  const folderDir = path.dirname(absoluteManifest)
  const { headers, authorization } = authHeaders(adminJwt)
  const { narrative, ...level } = manifest
  const result = await importLevel(baseUrl, folderDir, level, headers, authorization)
  const mystery = narrative ? await importNarrative(baseUrl, manifest.slug, manifest.title, narrative, headers, authorization) : undefined
  return { manifest, template: result.template, mystery, authoringLevelId: result.authoringLevelId, playableLevel: result.playableLevel }
}

// New self-contained import: a mystery folder (or single-file) with every level embedded.
// Accepts a directory (uses <dir>/mystery.json) or a manifest path; falls back to the
// legacy path when the file has no top-level `levels` array.
export async function importMystery(inputPath: string, baseUrl = 'http://localhost:8787', adminJwt = process.env.OSINT_ADMIN_JWT) {
  const resolved = path.resolve(inputPath)
  const manifestPath = (await stat(resolved)).isDirectory() ? path.join(resolved, 'mystery.json') : resolved
  const parsed = JSON.parse(await readFile(manifestPath, 'utf8')) as MysteryFile | MysteryManifest
  if (!Array.isArray((parsed as MysteryFile).levels)) return importMysteryTemplate(manifestPath, baseUrl, adminJwt)
  const file = parsed as MysteryFile
  const folderDir = path.dirname(manifestPath)
  const { headers, authorization } = authHeaders(adminJwt)
  const levels: LevelResult[] = []
  for (const level of file.levels) levels.push(await importLevel(baseUrl, folderDir, level, headers, authorization))
  const mystery = file.narrative ? await importNarrative(baseUrl, file.slug, file.title, file.narrative, headers, authorization) : undefined
  return { slug: file.slug, title: file.title, levels, mystery }
}

const invokedPath = process.argv[1] ? path.resolve(process.argv[1]) : ''
if (invokedPath === fileURLToPath(import.meta.url)) {
  const inputPath = process.argv[2]
  if (!inputPath) throw new Error('Usage: npm run mystery:import -- <mystery-folder | manifest.json>')
  const result = await importMystery(inputPath, process.env.OSINT_BOARD_URL)
  const playUrl = `${process.env.OSINT_BOARD_URL || 'http://localhost:8787'}/`
  const summary = 'levels' in result
    ? { mystery: result.mystery?.slug, levels: result.levels.map(l => `${l.template.slug}@v${l.template.currentVersion}`), playUrl }
    : { template: `${result.template.slug}@v${result.template.currentVersion}`, mystery: result.mystery?.slug, authoringLevelId: result.authoringLevelId, playableLevelId: result.playableLevel.id, playUrl }
  console.log(JSON.stringify(summary, null, 2))
}
