import { execFile } from 'child_process'
import { promisify } from 'util'
import type {
  DdevAddonRegistryEntry,
  DdevInstalledAddon,
  DdevProjectDetail,
  DdevProjectSummary,
  DdevSnapshot
} from '../shared/types'

const execFileAsync = promisify(execFile)

// GUI apps launched from Finder/Dock on macOS don't inherit the shell's PATH,
// so Homebrew-installed `ddev` at /opt/homebrew/bin can be invisible even
// though it works fine when launched from a terminal (`pnpm dev`).
const EXTRA_PATH_DIRS = ['/opt/homebrew/bin', '/usr/local/bin', '/opt/local/bin']
const ENV_WITH_DDEV_PATH = {
  ...process.env,
  PATH: [...EXTRA_PATH_DIRS, process.env.PATH].join(':')
}

export class DdevCliError extends Error {
  constructor(message: string) {
    super(message)
    this.name = 'DdevCliError'
  }
}

interface DdevJsonEnvelope<T> {
  level: string
  msg: T | string
  raw?: T
  time: string
}

async function execDdev<T>(args: string[], cwd?: string): Promise<DdevJsonEnvelope<T>> {
  let stdout: string
  try {
    ;({ stdout } = await execFileAsync('ddev', [...args, '--json-output'], {
      maxBuffer: 32 * 1024 * 1024,
      env: ENV_WITH_DDEV_PATH,
      cwd
    }))
  } catch (error) {
    const execError = error as { stdout?: string; stderr?: string; message: string }
    const parsed = tryParseLastJsonLine<DdevJsonEnvelope<T>>(execError.stdout ?? '')
    if (parsed?.msg && typeof parsed.msg === 'string') {
      throw new DdevCliError(parsed.msg)
    }
    if (execError.message.includes('ENOENT')) {
      throw new DdevCliError('ddev is not installed or not on PATH')
    }
    throw new DdevCliError(execError.stderr?.trim() || execError.message)
  }

  const envelope = tryParseLastJsonLine<DdevJsonEnvelope<T>>(stdout)
  if (!envelope) {
    throw new DdevCliError(`Could not parse ddev output: ${stdout.slice(0, 500)}`)
  }
  if (envelope.level === 'fatal' || envelope.level === 'error') {
    throw new DdevCliError(typeof envelope.msg === 'string' ? envelope.msg : 'ddev command failed')
  }
  return envelope
}

// Read commands where `raw` is always expected on success (describe): throws
// if it's missing, since that indicates something actually went wrong.
async function runDdevRead<T>(args: string[], cwd?: string): Promise<T> {
  const envelope = await execDdev<T>(args, cwd)
  if (envelope.raw === undefined) {
    throw new DdevCliError(
      typeof envelope.msg === 'string' ? envelope.msg : 'ddev returned no data'
    )
  }
  return envelope.raw
}

// List-style read commands are inconsistent about `raw` when there's nothing
// to report: `ddev list`/`ddev snapshot --list` include `raw: null`, but
// `ddev addon list --installed` omits `raw` entirely for "no add-ons found".
// Treat a missing `raw` as "empty" here rather than an error.
async function runDdevReadList<T>(args: string[], cwd?: string): Promise<T | undefined> {
  const envelope = await execDdev<T>(args, cwd)
  return envelope.raw
}

// ddev sometimes prints non-JSON progress lines before the final JSON envelope
// (opt-in prompts, deprecation notices); the envelope is always the last line.
function tryParseLastJsonLine<T>(output: string): T | null {
  const lines = output.trim().split('\n')
  for (let i = lines.length - 1; i >= 0; i--) {
    const line = lines[i].trim()
    if (!line.startsWith('{')) continue
    try {
      return JSON.parse(line) as T
    } catch {
      continue
    }
  }
  return null
}

export async function listProjects(): Promise<DdevProjectSummary[]> {
  const raw = await runDdevReadList<DdevProjectSummary[] | null>(['list'])
  return raw ?? []
}

export async function describeProject(name: string): Promise<DdevProjectDetail> {
  return runDdevRead<DdevProjectDetail>(['describe', name])
}

// `ddev snapshot restore` has no project-name flag — it relies on cwd to
// resolve the project, so every snapshot command runs with cwd = approot
// rather than passing the project name positionally like other commands.
export async function listSnapshots(name: string, approot: string): Promise<DdevSnapshot[]> {
  const raw = await runDdevReadList<Record<string, DdevSnapshot[] | null>>(
    ['snapshot', '--list'],
    approot
  )
  return raw?.[name] ?? []
}

export async function listAddonRegistry(): Promise<DdevAddonRegistryEntry[]> {
  const raw = await runDdevReadList<DdevAddonRegistryEntry[] | null>(['addon', 'list'])
  return raw ?? []
}

export async function listInstalledAddons(name: string): Promise<DdevInstalledAddon[]> {
  const raw = await runDdevReadList<DdevInstalledAddon[] | null>([
    'addon',
    'list',
    '--installed',
    '--project',
    name
  ])
  return raw ?? []
}
