#!/usr/bin/env bun
import { execSync } from "node:child_process"
import fs from "node:fs/promises"
import path from "node:path"

import { resetColorIndex } from "./colors"
import { checkIsMonorepo, dedupeSingletons, linkPackages } from "./link-packages"
import { setupOverrides } from "./manage-overrides"
import { runConcurrently, type CommandConfig } from "./runner"
import { setupVSCode } from "./vscode-config"

const cwd = process.env.PWD!

interface Config {
  linkPackages: string[]
  dedupeSingletons?: boolean | string[]
}

const exec = (cmd: string) => {
  const fullCmd = `bunx ${cmd}`
  console.info(`Executing: ${fullCmd}`)
  try {
    // bunx is optimized for running scripts
    execSync(fullCmd, { cwd, stdio: `inherit` }) // make execSync process to use the parent's: "stdin", "stdout", "stderr" streams
  } catch {
    process.exit(1)
  }
}

const getConfig = async (): Promise<Config | null> =>
  import(path.join(cwd, `10stars.config`)).catch(() => null)

const getOxlintCommonCmd = async () => {
  const isMonorepo = await checkIsMonorepo(cwd)
  const paths = [`src`, `scripts`, `.`].map((p) => (isMonorepo ? `*/${p}` : p)).join(` `)
  const quiet = process.env.CI ? `--quiet` : `` // no logs for warnings https://oxc.rs/docs/guide/usage/linter/cli.html#handle-warnings
  // Use standard oxlint which picks up localized .oxlintrc.json if present, or defaults
  return `${paths} ${quiet} --ignore-pattern '**/node_modules/**' --ignore-pattern '**/dist/**'`
}

const typecheckAndWatch = () => exec(`tsc --project ./tsconfig.json --noEmit --watch`)

const actions = {
  prepare: {
    info: `Runs husky and links packages`,
    action: async () => {
      if (process.env.CI) return

      // we must copy it first, because the following steps might have formatting cmds
      const oxfmtSrc = path.join(new URL("../.oxfmtrc.json", import.meta.url).pathname, ``)
      const oxfmtDest = path.join(cwd, `.oxfmtrc.json`)
      await fs.cp(oxfmtSrc, oxfmtDest, { force: true })

      exec(`husky`) // husky is intended only for local development (not in CI)
      await setupVSCode(cwd) // create/update .vscode directory with standardized extensions.json and settings.json
      await setupOverrides(cwd) // sync standardized package.json overrides

      const config = await getConfig()
      if (config?.linkPackages) {
        const linkedPaths = await linkPackages(cwd, config.linkPackages)
        if (config.dedupeSingletons !== false) {
          const singletons = Array.isArray(config.dedupeSingletons)
            ? config.dedupeSingletons
            : undefined
          await dedupeSingletons(cwd, config.linkPackages, linkedPaths, singletons)
        }
      }
    },
  },
  typecheckWatch: {
    info: "Typecheck code (in watch mode)",
    action: typecheckAndWatch,
  },
  ts: {
    info: `alias for 'typecheckWatch'`,
    action: typecheckAndWatch,
  },
  typecheck: {
    info: `Typecheck code (no watch)`,
    action: () => exec(`tsc --project ./tsconfig.json --noEmit`),
  },
  lint: {
    info: `Format & Lint code (no watch)`,
    action: async () => {
      exec(`--bun oxfmt`) // split for better error tracing; --bun prevents ESM resolution issues
      exec(`--bun oxlint --fix --type-aware ${await getOxlintCommonCmd()}`)
    },
  },
  precommit: {
    info: `Run pre-commit hooks`,
    action: async () => {
      const fs = await import(`node:fs`)
      const commitMsgFile = process.argv[3] || `.git/COMMIT_EDITMSG`
      const message = fs.readFileSync(commitMsgFile, `utf8`)
      const firstLine = message.split(`\n`)[0] || ``
      const VALID_LABELS = [
        // labels from keepachangelog
        `ADDED`,
        `CHANGED`,
        `DEPRECATED`,
        `REMOVED`,
        `FIXED`,
        `SECURITY`,
        `FIX`, // not in keepachangelog, but a common convention `fix:`
        `DOCS`, // not in keepachangelog, but a common convention `docs:`
        `CHORE`, // not in keepachangelog, but a common convention `chore:`
        `REFACTOR`, // not in keepachangelog, but a common convention `refactor:`
        `REFACTORED`, // not in keepachangelog
        `PERF`, // not in keepachangelog, but a common convention `perf:`
        `TEST`, // not in keepachangelog, but a common convention `test:`
      ]
      const isValid = VALID_LABELS.some(
        (label) =>
          firstLine.startsWith(`${label}:`) || firstLine.startsWith(`${label.toLowerCase()}:`),
      )

      if (!isValid) {
        console.error(`Invalid commit message format.`)
        console.error(`Must start with one of: ${VALID_LABELS.map((l) => `${l}:`).join(`, `)}`)
        process.exit(1)
      }
    },
  },
  run: {
    info: `Run multiple commands concurrently (usage: 10config run -l label1 "cmd1" -l label2 "cmd2")`,
    action: async () => {
      const args = process.argv.slice(3) // skip 'node', 'config', 'run'

      const printHelp = () => {
        console.log(`
Run multiple commands concurrently with colored prefixes

Usage:
  10config run --label <name> "<command>" [--label <name> "<command>" ...]
  10config run -l <name> "<command>" [-l <name> "<command>" ...]
  10config run "<command1>" "<command2>" ...

Examples:
  10config run -l server "bun run dev.server" -l client "bun run dev.client"
  10config run -l hono "bun run dev.hono" -l astro "bun run dev.astro"
`)
      }

      if (args.includes("-h") || args.includes("--help")) {
        printHelp()
        process.exit(0)
      }

      const commands: CommandConfig[] = []
      let i = 0

      while (i < args.length) {
        const arg = args[i]
        if (arg === "-l" || arg === "--label") {
          const label = args[i + 1]
          const command = args[i + 2]
          if (!label || !command) {
            console.error(`Error: --label requires a label and a command`)
            console.error(`Usage: 10config run --label <name> "<command>"`)
            process.exit(1)
          }
          commands.push({ label, command })
          i += 3
        } else {
          commands.push({ command: arg! })
          i++
        }
      }

      if (commands.length === 0) {
        printHelp()
        process.exit(0)
      }

      resetColorIndex()
      await runConcurrently(commands)
    },
  },
  killPort: {
    info: `Kill process on port(s)`,
    action: () => {
      const ports = process.argv.slice(3)
      if (!ports.length) {
        console.error(`At least one port required`)
        process.exit(1)
      }
      for (const port of ports) {
        try {
          if (process.platform === "win32") {
            execSync(
              `for /f "tokens=5" %a in ('netstat -aon ^| findstr :${port}') do taskkill /F /PID %a`,
              { stdio: "inherit" },
            )
          } else {
            execSync(`lsof -ti :${port} | xargs kill -9`, { stdio: "inherit" })
          }
          console.info(`Killed process on port ${port}`)
        } catch {
          console.info(`No process found on port ${port}`)
        }
      }
    },
  },
} satisfies Record<string, { info: string; action: () => void | Promise<void> }>

export const run = async () => {
  const cmd = process.argv[2]
  const availableActions = Object.keys(actions)

  const getAvailableCommands = () =>
    availableActions
      .map((action) => `  ${action} - ${actions[action as keyof typeof actions].info}`)
      .join("\n")

  if (!cmd || cmd === "-h" || cmd === "--help") {
    console.log(`Usage: 10config <command> [options]\n`)
    console.log(`Available commands:`)
    console.log(getAvailableCommands())
    process.exit(0)
  }

  if (!availableActions.includes(cmd)) {
    throw new Error(
      [
        `Unknown command: ${cmd}\n`,
        `Example CLI cmd: $ 10config lint`,
        `Available commands:`,
        getAvailableCommands(),
        "",
      ].join(`\n`),
    )
  }
  await actions[cmd as keyof typeof actions].action()
}

void run()
