#!/usr/bin/env bun

import { Schema, SchemaAST } from "effect"
import { format } from "prettier"
import { ThemeDefinition, ThemeDocument } from "@opencode-ai/theme/tui"

const target = import.meta.dir + "/../snippets/generated/theme-tokens.mdx"
const root = requireObject(ThemeDefinition.ast)
const hue = requireObject(requireField(root, "hue").type)
const hueNames = hue.propertySignatures.map((field) => String(field.name))
const hueSteps = requireObject(requireField(hue, hueNames[0]).type).propertySignatures.map((field) =>
  String(field.name),
)
const contexts = root.propertySignatures
  .map((field) => String(field.name))
  .filter((name) => name.startsWith("@context:"))
const tokens = root.propertySignatures
  .filter((field) => {
    const name = String(field.name)
    return name !== "hue" && name !== "categorical" && !name.startsWith("@context:")
  })
  .flatMap((field) => tokenPaths(field.type, String(field.name)))
const groups = Map.groupBy(tokens, (token) =>
  token
    .split(".")
    .slice(0, token.split(".").length > 2 ? 2 : 1)
    .join("."),
)
const table = [...groups]
  .map(([group, values]) => `| \`${group}\` | ${values.map((value) => `\`${value}\``).join("<br />")} |`)
  .join("\n")
const example = {
  version: 2,
  light: {
    hue: {
      accent: "$hue.purple",
      interactive: "$hue.purple",
    },
    text: {
      default: "$hue.neutral.900",
    },
    background: {
      default: "#fafafa",
    },
  },
  dark: {
    mergeMode: true,
    text: {
      default: "$hue.neutral.100",
    },
    background: {
      default: "#101014",
    },
  },
} satisfies ThemeDocument
Schema.decodeUnknownSync(ThemeDocument)(example)
const output = await format(
  `{/* Generated by packages/www/script/generate-theme-tokens.ts. Do not edit. */}

\`\`\`json title="my-theme.json"
${JSON.stringify(example, null, 2)}
\`\`\`

## Token reference

This reference is generated from the Effect schema in
\`@opencode-ai/theme/tui\`. Changes to the runtime schema update
this section through \`bun run generate\`.

### Hue tokens

Every hue is a ${hueSteps.length}-step scale. Define a scale with all of these
steps, or alias it to another hue with a value such as \`$hue.blue\`.

| | Values |
| --- | --- |
| Hues | ${hueNames.map((name) => `\`${name}\``).join(", ")} |
| Steps | ${hueSteps.map((step) => `\`${step}\``).join(", ")} |

Reference a hue color as \`$hue.<name>.<step>\`, for example
\`$hue.interactive.500\`.

### Semantic tokens

Semantic values can reference another token by prefixing its path with \`$\`,
for example \`$text.default\`. Stateful tokens inherit their \`default\`
value when a state is omitted.

| Group | Tokens |
| --- | --- |
${table}

### Contexts

${contexts.map((context) => `\`${context}\``).join(" and ")} accept partial
overrides of the semantic tokens above. Components apply these contexts to
surfaces that need different contrast without changing the base theme.
`,
  { parser: "mdx", printWidth: 120, semi: false },
)

if (process.argv.includes("--check")) {
  const current = await Bun.file(target).text()
  if (current === output) process.exit(0)
  console.error("Generated theme token documentation is stale. Run `bun run generate` from packages/www.")
  process.exit(1)
}

await Bun.write(target, output)

function requireObject(ast: SchemaAST.AST): SchemaAST.Objects {
  if (SchemaAST.isObjects(ast)) return ast
  if (SchemaAST.isUnion(ast)) {
    const object = ast.types.map(findObject).find((value) => value !== undefined)
    if (object) return object
  }
  throw new Error(`Expected an object schema, received ${ast._tag}`)
}

function findObject(ast: SchemaAST.AST): SchemaAST.Objects | undefined {
  if (SchemaAST.isObjects(ast)) return ast
  if (SchemaAST.isUnion(ast)) return ast.types.map(findObject).find((value) => value !== undefined)
  if (SchemaAST.isSuspend(ast)) return findObject(ast.thunk())
}

function requireField(ast: SchemaAST.Objects, name: string) {
  const field = ast.propertySignatures.find((field) => String(field.name) === name)
  if (field) return field
  throw new Error(`Theme schema field not found: ${name}`)
}

function tokenPaths(ast: SchemaAST.AST, prefix: string): string[] {
  const object = findObject(ast)
  if (!object || object.propertySignatures.length === 0) return [prefix]
  return object.propertySignatures.flatMap((field) => tokenPaths(field.type, `${prefix}.${String(field.name)}`))
}
