import { trimTrailingEmpty } from "./utils.ts"

export type Command = {
  line: number
  raw: string
  command: string
  expected: string[]
  exitCode: number | "*" | null
}

export type Directive =
  | { type: "setup"; path: string; line: number }
  | { type: "env"; key: string; value: string; line: number }

export type ShoutFile = {
  path: string
  commands: Command[]
  directives: Directive[]
  teardownCommands: Command[]
}

function stripComment(line: string): string {
  // Strip trailing # comment from command line
  // Be careful not to strip # inside quotes
  let inSingle = false
  let inDouble = false
  for (let i = 0; i < line.length; i++) {
    const ch = line[i]
    if (ch === "'" && !inDouble) inSingle = !inSingle
    else if (ch === '"' && !inSingle) inDouble = !inDouble
    else if (ch === "#" && !inSingle && !inDouble) {
      return line.slice(0, i).trimEnd()
    }
  }
  return line
}

/** A line like "$# ..." or "$ # ..." — a comment, not a real command */
export function isCommentLine(line: string): boolean {
  return line.startsWith("$#") || (line.startsWith("$ ") && stripComment(line.slice(2)) === "")
}

function parseExitCode(lines: string[]): {
  lines: string[]
  exitCode: number | "*" | null
} {
  if (lines.length === 0) return { lines, exitCode: null }

  const last = lines[lines.length - 1]!
  const match = last.match(/^\[(\d+|\*)\]$/)
  if (match) {
    const code = match[1] === "*" ? "*" as const : parseInt(match[1]!, 10)
    return { lines: lines.slice(0, -1), exitCode: code }
  }

  return { lines, exitCode: null }
}

function parseEnvDirective(path: string, line: string, lineNum: number): { key: string; value: string } {
  const rest = line.slice(5).trim()
  const eq = rest.indexOf("=")
  if (eq <= 0) {
    throw new Error(`${path}:${lineNum}: malformed @env directive (expected KEY=VALUE): ${line}`)
  }
  return { key: rest.slice(0, eq), value: rest.slice(eq + 1) }
}

export function parseSetup(path: string, content: string): ShoutFile {
  const rawLines = content.split("\n")

  if (rawLines.length > 0 && rawLines[rawLines.length - 1] === "") {
    rawLines.pop()
  }

  const commands: Command[] = []
  const teardownCommands: Command[] = []
  const directives: Directive[] = []

  for (let i = 0; i < rawLines.length; i++) {
    const line = rawLines[i]!

    if (line === "" || line.startsWith("#")) continue

    if (line.startsWith("@")) {
      if (line.startsWith("@env ")) {
        const { key, value } = parseEnvDirective(path, line, i + 1)
        directives.push({ type: "env", key, value, line: i + 1 })
      } else if (line.startsWith("@teardown ")) {
        const cmd = line.slice(10)
        if (!cmd.trim()) {
          throw new Error(`${path}:${i + 1}: @teardown requires a command`)
        }
        teardownCommands.push({
          line: i + 1,
          raw: line,
          command: stripComment(cmd),
          expected: [],
          exitCode: null,
        })
      } else if (line.startsWith("@setup ")) {
        throw new Error(`${path}:${i + 1}: @setup not allowed in setup files`)
      } else {
        throw new Error(`${path}:${i + 1}: unknown directive: ${line}`)
      }
      continue
    }

    commands.push({
      line: i + 1,
      raw: line,
      command: stripComment(line),
      expected: [],
      exitCode: null,
    })
  }

  return { path, commands, directives, teardownCommands }
}

export function parse(path: string, content: string): ShoutFile {
  const rawLines = content.split("\n")

  // Remove trailing newline (spec: "Trailing newline on the file is ignored")
  if (rawLines.length > 0 && rawLines[rawLines.length - 1] === "") {
    rawLines.pop()
  }

  const commands: Command[] = []
  const teardownCommands: Command[] = []
  const directives: Directive[] = []
  let current: Command | null = null
  let seenCommand = false

  for (let i = 0; i < rawLines.length; i++) {
    const line = rawLines[i]!

    if (!seenCommand && line.startsWith("@")) {
      if (line.startsWith("@setup ")) {
        const setupPath = line.slice(7).trim()
        if (!setupPath) {
          throw new Error(`${path}:${i + 1}: @setup requires a file path`)
        }
        directives.push({ type: "setup", path: setupPath, line: i + 1 })
      } else if (line.startsWith("@teardown ")) {
        const cmd = line.slice(10)
        if (!cmd.trim()) {
          throw new Error(`${path}:${i + 1}: @teardown requires a command`)
        }
        teardownCommands.push({
          line: i + 1,
          raw: line,
          command: stripComment(cmd),
          expected: [],
          exitCode: null,
        })
      } else if (line.startsWith("@env ")) {
        const { key, value } = parseEnvDirective(path, line, i + 1)
        directives.push({ type: "env", key, value, line: i + 1 })
      } else {
        throw new Error(`${path}:${i + 1}: unknown directive: ${line}`)
      }
      continue
    }

    if (isCommentLine(line)) {
      // Comment line like "$# ..." or "$ # ..." — finalize current and skip
      seenCommand = true
      if (current) {
        const trimmed = trimTrailingEmpty(current.expected)
        const { lines: expectedLines, exitCode } = parseExitCode(trimmed)
        current.expected = trimTrailingEmpty(expectedLines)
        current.exitCode = exitCode
        commands.push(current)
      }
      current = null
    } else if (line.startsWith("\\$ ") && current) {
      // Escaped dollar-space: literal expected output starting with "$ "
      current.expected.push(line.slice(1))
    } else if (line.startsWith("$ ")) {
      seenCommand = true
      if (current) {
        const trimmed = trimTrailingEmpty(current.expected)
        const { lines: expectedLines, exitCode } = parseExitCode(trimmed)
        current.expected = trimTrailingEmpty(expectedLines)
        current.exitCode = exitCode
        commands.push(current)
      }

      current = {
        line: i + 1,
        raw: line,
        command: stripComment(line.slice(2)),
        expected: [],
        exitCode: null,
      }
    } else if (current) {
      current.expected.push(line)
    }
  }

  if (current) {
    const trimmed = trimTrailingEmpty(current.expected)
    const { lines: expectedLines, exitCode } = parseExitCode(trimmed)
    current.expected = trimTrailingEmpty(expectedLines)
    current.exitCode = exitCode
    commands.push(current)
  }

  return { path, commands, directives, teardownCommands }
}
