/**
 * Lenient `SKILL.md` frontmatter parsing (spec §7).
 *
 * Other clients emit malformed frontmatter — most commonly unquoted colons in
 * descriptions — so the default is lenient. We hand-roll a tiny parser rather
 * than pull in a YAML dependency: SKILL.md frontmatter is a flat key/value
 * block (plus block scalars for `description` and simple lists for
 * `allowedTools`), and taking `value = rest-of-line-after-first-colon` handles
 * the unquoted-colon case in a single pass — no quote-and-retry needed.
 *
 * | Condition            | Behavior                        |
 * |----------------------|---------------------------------|
 * | name/dir mismatch    | warn, load                      |
 * | name over 64 chars   | warn, load                      |
 * | invalid name chars   | warn, load                      |
 * | missing `description`| throw (caller skips)            |
 * | no frontmatter       | throw (caller skips)            |
 *
 * `strict: true` promotes every warning to a thrown error.
 */
import type { SkillMetadata } from './types'

export interface ParseWarning {
  code: 'name-dir-mismatch' | 'name-too-long' | 'name-invalid-chars'
  message: string
}

export interface ParsedSkill {
  metadata: SkillMetadata
  /** frontmatter stripped. */
  body: string
  warnings: Array<ParseWarning>
}

export class SkillParseError extends Error {
  override name = 'SkillParseError'
}

const NAME_RE = /^[a-z0-9-]+$/

/** Split leading `---` frontmatter from the body. Returns null if absent. */
function splitFrontmatter(
  raw: string,
): { frontmatter: string; body: string } | null {
  // Tolerate a BOM and leading blank lines before the opening fence.
  const text = raw.replace(/^﻿/, '')
  const match = /^\s*---\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n([\s\S]*))?$/.exec(
    text,
  )
  if (!match) return null
  return { frontmatter: match[1] ?? '', body: match[2] ?? '' }
}

/** Parse the flat frontmatter block into raw string/list/map values. */
function parseBlock(frontmatter: string): Record<string, unknown> {
  const lines = frontmatter.split(/\r?\n/)
  const out: Record<string, unknown> = {}
  let i = 0

  const indentOf = (s: string) => s.length - s.trimStart().length

  while (i < lines.length) {
    const line = lines[i]
    if (line === undefined) break
    if (line.trim() === '' || line.trimStart().startsWith('#')) {
      i++
      continue
    }
    // Top-level keys are unindented.
    if (indentOf(line) > 0) {
      i++
      continue
    }
    const colon = line.indexOf(':')
    if (colon === -1) {
      i++
      continue
    }
    const key = line.slice(0, colon).trim()
    const value = line.slice(colon + 1).trim()

    // Block scalar: `>` (folded) or `|` (literal), with optional chomp/indent.
    if (value === '>' || value === '|' || /^[>|][+-]?\d*$/.test(value)) {
      const folded = value.startsWith('>')
      const collected: Array<string> = []
      i++
      while (i < lines.length) {
        const next = lines[i]
        if (next === undefined) break
        if (next.trim() !== '' && indentOf(next) === 0) break
        collected.push(next)
        i++
      }
      // Strip the common leading indentation of the block.
      const nonEmpty = collected.filter((l) => l.trim() !== '')
      const minIndent = nonEmpty.length
        ? Math.min(...nonEmpty.map(indentOf))
        : 0
      const stripped = collected.map((l) => l.slice(minIndent))
      out[key] = folded
        ? stripped.join(' ').replace(/\s+/g, ' ').trim()
        : stripped.join('\n').trim()
      continue
    }

    // Empty value → indented children: a block list (`- x`) or a nested map
    // (`k: v`), one level deep.
    if (value === '') {
      const items: Array<string> = []
      const map: Record<string, string> = {}
      let j = i + 1
      while (j < lines.length) {
        const next = lines[j]
        if (next === undefined) break
        if (next.trim() === '') {
          j++
          continue
        }
        if (indentOf(next) === 0) break
        const t = next.trim()
        if (t.startsWith('- ')) {
          items.push(unquote(t.slice(2).trim()))
        } else {
          const c = t.indexOf(':')
          if (c === -1) break
          map[t.slice(0, c).trim()] = unquote(t.slice(c + 1).trim())
        }
        j++
      }
      if (items.length) out[key] = items
      else if (Object.keys(map).length) out[key] = map
      else out[key] = ''
      i = Math.max(j, i + 1)
      continue
    }

    // Inline list `[a, b]`.
    if (value.startsWith('[') && value.endsWith(']')) {
      out[key] = value
        .slice(1, -1)
        .split(',')
        .map((s) => unquote(s.trim()))
        .filter((s) => s !== '')
      i++
      continue
    }

    out[key] = unquote(value)
    i++
  }
  return out
}

function unquote(s: string): string {
  if (
    (s.startsWith('"') && s.endsWith('"')) ||
    (s.startsWith("'") && s.endsWith("'"))
  ) {
    return s.slice(1, -1)
  }
  return s
}

function asStringMap(value: unknown): Record<string, string> | undefined {
  if (typeof value !== 'object' || value === null) return undefined
  const out: Record<string, string> = {}
  for (const [k, v] of Object.entries(value)) {
    if (typeof v === 'string') out[k] = v
  }
  return Object.keys(out).length ? out : undefined
}

/**
 * Parse a `SKILL.md`. Throws {@link SkillParseError} for cases the spec says to
 * skip (no frontmatter, missing description). Non-fatal issues are returned as
 * `warnings`; `strict` turns them into throws.
 */
export function parseSkill(
  raw: string,
  opts: { dirName?: string; strict?: boolean } = {},
): ParsedSkill {
  const split = splitFrontmatter(raw)
  if (!split) {
    throw new SkillParseError('SKILL.md has no frontmatter block')
  }
  const block = parseBlock(split.frontmatter)

  const name = typeof block.name === 'string' ? block.name : undefined
  const description =
    typeof block.description === 'string' ? block.description : undefined

  if (!description) {
    throw new SkillParseError('SKILL.md is missing a `description`')
  }

  const warnings: Array<ParseWarning> = []
  const effectiveName = name ?? opts.dirName ?? ''

  if (name && opts.dirName && name !== opts.dirName) {
    warnings.push({
      code: 'name-dir-mismatch',
      message: `skill name "${name}" does not match directory "${opts.dirName}"`,
    })
  }
  if (effectiveName.length > 64) {
    warnings.push({
      code: 'name-too-long',
      message: `skill name "${effectiveName}" exceeds 64 characters`,
    })
  }
  if (effectiveName && !NAME_RE.test(effectiveName)) {
    warnings.push({
      code: 'name-invalid-chars',
      message: `skill name "${effectiveName}" contains characters outside [a-z0-9-]`,
    })
  }

  if (opts.strict && warnings.length) {
    throw new SkillParseError(warnings.map((w) => w.message).join('; '))
  }

  const metadata: SkillMetadata = {
    name: effectiveName,
    description,
    ...(typeof block.license === 'string' && { license: block.license }),
    ...(typeof block.compatibility === 'string' && {
      compatibility: block.compatibility,
    }),
    ...(Array.isArray(block.allowedTools) && {
      allowedTools: block.allowedTools.filter(
        (t): t is string => typeof t === 'string',
      ),
    }),
    ...((): { metadata?: Record<string, string> } => {
      const m = asStringMap(block.metadata)
      return m ? { metadata: m } : {}
    })(),
  }

  return { metadata, body: split.body.trim(), warnings }
}

/** Strip the frontmatter block, returning just the body. */
export function stripFrontmatter(raw: string): string {
  const split = splitFrontmatter(raw)
  return split ? split.body.trim() : raw.trim()
}
