#!/usr/bin/env node
// Parses src/lib/tapestry/grammar.ts and emits a Swift mirror under
// targets/watch/Generated/TapestryLetters.swift. The grammar is a
// canonicalised set of single ASCII letters with case-encoded valence.
// The server side (`app/Halflight/Support/TapestryGrammar.php`) is the
// source of truth — TS and Swift must both match it byte-for-byte.
//
// Parsing rather than codegen-from-codegen because grammar.ts is hand-
// authored and the simplest robust extraction is a regex over the
// `TAPESTRY_LETTERS = { L: 'like', ... }` object literal.

import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'

const HERE = dirname(fileURLToPath(import.meta.url))
const REPO_ROOT = join(HERE, '..', '..')
const GRAMMAR_TS = join(REPO_ROOT, 'src', 'lib', 'tapestry', 'grammar.ts')
const OUT = join(REPO_ROOT, 'targets', 'watch', 'Generated', 'TapestryLetters.swift')

function escapeSwift(s) {
  return String(s).replace(/\\/g, '\\\\').replace(/"/g, '\\"')
}

function parseLetters(src) {
  const blockMatch = src.match(/TAPESTRY_LETTERS\s*=\s*\{([\s\S]*?)\}\s*as\s+const/)
  if (!blockMatch) {
    throw new Error('gen-tapestry-letters: TAPESTRY_LETTERS object literal not found')
  }
  const body = blockMatch[1]
  const entries = []
  // Match `K: 'description',` lines; ignore comments and blank lines.
  const lineRe = /^\s*([A-Za-z]):\s*'([^']+)',?\s*$/gm
  let m
  while ((m = lineRe.exec(body)) !== null) {
    entries.push([m[1], m[2]])
  }
  if (entries.length === 0) {
    throw new Error('gen-tapestry-letters: no entries parsed from TAPESTRY_LETTERS')
  }
  // Sanity: expect 18 letters per the plan and grammar.ts comment.
  if (entries.length !== 18) {
    throw new Error(
      `gen-tapestry-letters: expected 18 letters, parsed ${entries.length}. Update this script if the grammar genuinely grew.`
    )
  }
  const maxBytesMatch = src.match(/MAX_BODY_BYTES\s*=\s*(\d+)/)
  if (!maxBytesMatch) {
    throw new Error('gen-tapestry-letters: MAX_BODY_BYTES not found')
  }
  return { entries, maxBodyBytes: Number(maxBytesMatch[1]) }
}

// Mirrors LETTER_CLASS in src/components/tapestry/TapestryRow.tsx. Hand-
// authored on the TS side too — the grammar.ts source doesn't carry a
// machine-readable class tag, so we duplicate the truth here. CI-side
// drift is caught by the `_AllLettersClassed` compile-time assertion in
// the TSX file plus the count-mismatch guard in `parseLetters` above.
const LETTER_CLASS = {
  P: 'authored',
  C: 'authored',
  M: 'authored',
  V: 'authored',
  E: 'authored',
  T: 'authored',
  L: 'social',
  R: 'social',
  B: 'social',
  F: 'social',
  D: 'communication',
  N: 'communication',
  s: 'consumption',
  v: 'consumption',
  e: 'consumption',
  u: 'corrective',
  d: 'corrective',
  x: 'corrective',
}

export function generate() {
  const src = readFileSync(GRAMMAR_TS, 'utf8')
  const { entries, maxBodyBytes } = parseLetters(src)
  const alphabet = entries.map(([k]) => k).join('')
  const lines = entries
    .map(([k, desc]) => `        "${k}": "${escapeSwift(desc)}"`)
    .join(',\n')
  // Sanity: every grammar letter must have a class entry. Mirrors the
  // `_AllLettersClassed` compile-time assertion in TapestryRow.tsx.
  for (const [k] of entries) {
    if (!(k in LETTER_CLASS)) {
      throw new Error(
        `gen-tapestry-letters: letter '${k}' has no LETTER_CLASS entry. Update this script's table to match src/components/tapestry/TapestryRow.tsx.`
      )
    }
  }
  const classLines = entries
    .map(([k]) => `        "${k}": .${LETTER_CLASS[k]}`)
    .join(',\n')
  const swift = `// AUTO-GENERATED — do not edit. Source: src/lib/tapestry/grammar.ts
// Regenerate via \`node targets/_scripts/gen-tapestry-letters.mjs\` (runs at
// prebuild time via plugins/withWatchGenerated.js).
//
// Mirror of the server's TapestryGrammar.php — must match byte-for-byte.

import Foundation

/// Five-class palette mirror of LETTER_CLASS in
/// src/components/tapestry/TapestryRow.tsx. Drives the watch tapestry
/// view's per-letter accent tinting; the iOS \`accent\` family is the
/// active account's accent variant (see AccentVariants).
enum TapestryLetterClass {
    case authored
    case social
    case communication
    case consumption
    case corrective
    case unknown
}

enum TapestryLetters {
    static let alphabet: String = "${alphabet}"
    static let alphabetSet: Set<Character> = Set("${alphabet}")
    static let maxBodyBytes = ${maxBodyBytes}

    static let descriptions: [Character: String] = [
${lines}
    ]

    static let classes: [Character: TapestryLetterClass] = [
${classLines}
    ]

    static func isLetter(_ c: Character) -> Bool {
        alphabetSet.contains(c)
    }

    static func isValidBody(_ body: String) -> Bool {
        if body.isEmpty || body.count > maxBodyBytes { return false }
        for c in body where !alphabetSet.contains(c) { return false }
        return true
    }

    static func letterClass(of c: Character) -> TapestryLetterClass {
        classes[c] ?? .unknown
    }
}
`
  mkdirSync(dirname(OUT), { recursive: true })
  writeFileSync(OUT, swift)
  return { path: OUT, count: entries.length }
}

if (import.meta.url === `file://${process.argv[1]}`) {
  const { path, count } = generate()
  console.log(`gen-tapestry-letters: wrote ${path} (${count} letters)`)
}
