import { Command } from '@cliffy/command'
import { ensureDir } from '@std/fs'
import { join } from '@std/path'
import {
  CONFIG_JSON,
  PBF_DIR,
  POLY_DIR,
  PROCESS_LUA,
  TILES_MANIFEST,
  TILES_OUT_DIR,
} from '../shared/paths.ts'
import { leafName } from '../shared/regions.ts'
import { run } from '../shared/run.ts'

type TileEntry = {
  id: string
  label: string
  description: string
  filename: string
  path: string
  group: string
  bounds?: [number, number, number, number]
}

async function bboxFromPbfHeader(pbfPath: string): Promise<string | null> {
  try {
    const proc = new Deno.Command('osmium', {
      args: ['fileinfo', '-j', pbfPath],
      stdout: 'piped',
      stderr: 'null',
    })
    const { stdout } = await proc.output()
    const info = JSON.parse(new TextDecoder().decode(stdout))
    const box = info?.header?.boxes?.[0]
    if (Array.isArray(box) && box.length === 4 && box.some((v) => v !== 0)) {
      return box.join(',')
    }
  } catch { /* osmium unavailable */ }
  return null
}

async function bboxFromPoly(
  region: string,
): Promise<[number, number, number, number] | null> {
  const parts = region.split('/')
  const polyPath = join(
    POLY_DIR,
    ...parts.slice(0, -1),
    `${leafName(region)}.poly`,
  )
  try {
    const text = await Deno.readTextFile(polyPath)
    const coords = text.split('\n').flatMap((line) => {
      const parts = line.trim().split(/\s+/)
      if (parts.length !== 2) return []
      const lon = parseFloat(parts[0])
      const lat = parseFloat(parts[1])
      return isNaN(lon) || isNaN(lat) ? [] : [{ lon, lat }]
    })
    if (!coords.length) return null
    const west = Math.min(...coords.map((c) => c.lon))
    const south = Math.min(...coords.map((c) => c.lat))
    const east = Math.max(...coords.map((c) => c.lon))
    const north = Math.max(...coords.map((c) => c.lat))
    return [west, south, east, north]
  } catch { /* no poly file */ }
  return null
}

async function upsertManifest(entry: TileEntry): Promise<void> {
  let tiles: TileEntry[] = []
  try {
    tiles = JSON.parse(await Deno.readTextFile(TILES_MANIFEST))
  } catch { /* first run */ }
  const idx = tiles.findIndex((t) => t.id === entry.id)
  if (idx >= 0) tiles[idx] = entry
  else tiles.push(entry)
  await Deno.writeTextFile(
    TILES_MANIFEST,
    JSON.stringify(tiles, null, 2) + '\n',
  )
}

const PBF_SUFFIX = '-latest.osm.pbf'
// Planet is handled by `build:world` and uses a different config/lua pair.
const EXCLUDED_LEAVES = new Set(['planet'])

// Expand a region (or the whole osm tree) into concrete leaf regions to
// build by walking `data/osm/`. A "leaf" is a `<name>-latest.osm.pbf` file
// with no sibling directory of the same name — so if `data/osm/asia/japan/`
// exists, passing `asia/japan` builds every prefecture pbf inside that
// directory rather than looking for `asia/japan-latest.osm.pbf`.
async function collectLeafRegions(region?: string): Promise<string[]> {
  const parts = region ? region.split('/') : []
  const dirPath = join(PBF_DIR, ...parts)
  const dirStat = await Deno.stat(dirPath).catch(() => null)

  if (dirStat?.isDirectory) {
    const leaves: string[] = []
    for await (const entry of Deno.readDir(dirPath)) {
      if (entry.name.startsWith('.')) continue
      if (entry.isDirectory) {
        const child = [...parts, entry.name].join('/')
        leaves.push(...(await collectLeafRegions(child)))
      } else if (entry.isFile && entry.name.endsWith(PBF_SUFFIX)) {
        const base = entry.name.slice(0, -PBF_SUFFIX.length)
        if (EXCLUDED_LEAVES.has(base)) continue
        const sibling = await Deno.stat(join(dirPath, base)).catch(() => null)
        if (sibling?.isDirectory) continue
        leaves.push([...parts, base].join('/'))
      }
    }
    return leaves
  }

  if (region) {
    const parent = join(PBF_DIR, ...parts.slice(0, -1))
    const fileName = `${leafName(region)}${PBF_SUFFIX}`
    const fileStat = await Deno.stat(join(parent, fileName)).catch(() => null)
    if (fileStat?.isFile) return [region]
  }
  return []
}

async function buildRegion(region: string): Promise<void> {
  const parts = region.split('/')
  const id = leafName(region)
  const pbfPath = join(
    PBF_DIR,
    ...parts.slice(0, -1),
    `${id}-latest.osm.pbf`,
  )
  const subDir = join(TILES_OUT_DIR, ...parts.slice(0, -1))
  const outPath = join(subDir, `${id}.pmtiles`)

  if (!(await Deno.stat(pbfPath).catch(() => null))) {
    throw new Error(
      `PBF not found: ${pbfPath}\nRun: deno task data download:osm ${region}`,
    )
  }

  await ensureDir(subDir)

  // Resolve bounding box for tilemaker so it can read regional shapefiles
  // (ocean, landcover, etc.) without scanning the whole world.
  // Strategy: try PBF header first (fast), fall back to poly file (reliable
  // for osmium-extracted files which often have no bbox in their header).
  const bboxArgs: string[] = []
  const pbfBbox = await bboxFromPbfHeader(pbfPath)
  const polyBbox = await bboxFromPoly(region)
  const bbox = pbfBbox ?? (polyBbox ? polyBbox.join(',') : null)
  if (bbox) bboxArgs.push('--bbox', bbox)

  console.log(`Building tiles for ${region} ...`)

  await run('tilemaker', [
    '--input',
    pbfPath,
    '--output',
    outPath,
    '--config',
    CONFIG_JSON,
    '--process',
    PROCESS_LUA,
    ...bboxArgs,
  ])

  console.log(`Saved: ${outPath}`)

  const { size } = await Deno.stat(outPath)
  const sizeStr = size < 1024 * 1024
    ? `${Math.round(size / 1024)} KB`
    : `${Math.round(size / (1024 * 1024))} MB`
  const label = id.split('-').map((w) => w[0].toUpperCase() + w.slice(1))
    .join(' ')
  const relativePath = [...parts.slice(0, -1), `${id}.pmtiles`].join('/')
  await upsertManifest({
    id,
    label,
    description: `Detailed street map · ~${sizeStr}`,
    filename: relativePath,
    path: `/static/tiles/${relativePath}`,
    group: parts.slice(0, -1).join('/'),
    bounds: polyBbox ?? undefined,
  })
  console.log(`Updated tiles manifest`)
}

export const buildCmd = new Command()
  .name('build')
  .description(
    'Run tilemaker to generate .pmtiles files from regional .osm.pbf ' +
      'extracts. Omit the region to build every leaf region, or pass a ' +
      'parent (e.g. asia/japan) to build all of its children.',
  )
  .arguments('[region:string]')
  .action(async (_, region) => {
    const regions = (await collectLeafRegions(region)).sort()
    if (!regions.length) {
      throw new Error(
        region
          ? `No .osm.pbf files found for ${region}\nRun: deno task data download:osm ${region}`
          : `No .osm.pbf files found under ${PBF_DIR}`,
      )
    }

    console.log(
      `Building ${regions.length} region(s):\n  ${regions.join('\n  ')}`,
    )

    const failures: string[] = []
    for (const r of regions) {
      try {
        await buildRegion(r)
      } catch (error) {
        console.error(`Failed to build ${r}: ${(error as Error).message}`)
        failures.push(r)
      }
    }

    if (failures.length) {
      throw new Error(
        `Failed to build ${failures.length} of ${regions.length} region(s): ${
          failures.join(', ')
        }`,
      )
    }
  })
