#!/usr/bin/env node
/**
 * 固件仓库校验器(零依赖)。
 *
 *   node scripts/validate.mjs
 *
 * 规则见 README.md 第五节。站点同步时会用同一套规则再校验一次。
 */
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'
import { dirname, join, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'

const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const CHIPS = ['ws63', 'bs21e']

const SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/
const FILE_RE = /^[a-z0-9][a-z0-9.-]*-[0-9]{8}\.fwpkg$/
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/
const VERSION_RE = /^\d+\.\d+(?:\.\d+)?$/

const errors = []
const warnings = []
let projects = 0
let files = 0

function fail(where, message) {
  errors.push(`${where}: ${message}`)
}

function warn(where, message) {
  warnings.push(`${where}: ${message}`)
}

function isPlainObject(value) {
  return value !== null && typeof value === 'object' && !Array.isArray(value)
}

function checkLocalized(where, value, field, maxZh = 400, maxEn = 600) {
  if (!isPlainObject(value)) return fail(where, `${field} 必须是对象 { "zh-CN", "en" }`)
  for (const [locale, max] of [
    ['zh-CN', maxZh],
    ['en', maxEn],
  ]) {
    const text = value[locale]
    if (typeof text !== 'string' || text.trim() === '') {
      fail(where, `${field}.${locale} 不能为空`)
    } else if (text.length > max) {
      fail(where, `${field}.${locale} 超过 ${max} 字符`)
    }
  }
  for (const key of Object.keys(value)) {
    if (key !== 'zh-CN' && key !== 'en') fail(where, `${field} 有未知语言 ${key}`)
  }
}

function compact(date) {
  return date.replaceAll('-', '')
}

for (const chip of CHIPS) {
  const chipDir = join(ROOT, chip)
  if (!existsSync(chipDir) || !statSync(chipDir).isDirectory()) {
    fail(chip, '缺少芯片目录')
    continue
  }

  for (const entry of readdirSync(chipDir)) {
    const projectDir = join(chipDir, entry)
    if (!statSync(projectDir).isDirectory()) {
      warn(chip, `忽略文件 ${entry}`)
      continue
    }

    projects++
    const where = `${chip}/${entry}`
    const errorCountBefore = errors.length
    const manifestPath = join(projectDir, 'firmware.json')

    if (!SLUG_RE.test(entry)) {
      fail(where, '目录名必须是小写英文 + 连字符(slug 规则)')
    }
    if (!existsSync(manifestPath)) {
      fail(where, '缺少 firmware.json')
      continue
    }

    let manifest
    try {
      manifest = JSON.parse(readFileSync(manifestPath, 'utf8'))
    } catch (error) {
      fail(where, `firmware.json 解析失败:${error.message}`)
      continue
    }

    if (!isPlainObject(manifest)) {
      fail(where, 'firmware.json 必须是对象')
      continue
    }
    if (manifest.schema !== 1) fail(where, `schema 必须是 1(当前 ${manifest.schema})`)
    if (manifest.slug !== entry) fail(where, `slug "${manifest.slug}" 与目录名不一致`)
    if (manifest.chip !== chip) fail(where, `chip "${manifest.chip}" 与所在目录不一致`)
    if (typeof manifest.category !== 'string' || manifest.category.trim() === '') {
      fail(where, 'category 不能为空')
    } else if (manifest.category.length > 40) {
      fail(where, 'category 超过 40 字符')
    }
    checkLocalized(where, manifest.name, 'name')
    checkLocalized(where, manifest.summary, 'summary')
    if (manifest.notes !== undefined) checkLocalized(where, manifest.notes, 'notes')
    if (manifest.author !== undefined && typeof manifest.author !== 'string') {
      fail(where, 'author 必须是字符串')
    }
    if (manifest.license !== undefined && typeof manifest.license !== 'string') {
      fail(where, 'license 必须是字符串')
    }

    if (typeof manifest.updated !== 'string' || !DATE_RE.test(manifest.updated)) {
      fail(where, 'updated 必须是 YYYY-MM-DD')
    }

    if (!Array.isArray(manifest.firmware) || manifest.firmware.length === 0) {
      fail(where, 'firmware 必须是非空数组')
      continue
    }

    const listed = new Set()
    const variants = new Map()
    const dates = []

    for (const [index, item] of manifest.firmware.entries()) {
      const at = `${where}.firmware[${index}]`
      if (!isPlainObject(item)) {
        fail(at, '必须是对象')
        continue
      }
      const file = item.file
      if (typeof file !== 'string' || !FILE_RE.test(file)) {
        fail(at, `文件名不合法:${file}`)
        continue
      }
      if (!file.startsWith(`${entry}-`)) {
        fail(at, `文件名必须以项目 slug "${entry}-" 开头`)
      }
      if (listed.has(file)) fail(at, `重复登记 ${file}`)
      listed.add(file)

      const full = join(projectDir, file)
      if (!existsSync(full)) {
        fail(at, `文件不存在:${file}`)
        continue
      }
      files++

      if (typeof item.date !== 'string' || !DATE_RE.test(item.date)) {
        fail(at, `date 必须是 YYYY-MM-DD(当前 ${item.date})`)
      } else {
        dates.push(item.date)
        if (!file.endsWith(`-${compact(item.date)}.fwpkg`)) {
          fail(at, `文件名日期与 date "${item.date}" 不一致`)
        }
      }

      if (item.variant !== undefined) {
        if (typeof item.variant !== 'string' || !SLUG_RE.test(item.variant)) {
          fail(at, `variant 不合法:${item.variant}`)
        } else {
          if (variants.has(item.variant)) {
            fail(at, `variant "${item.variant}" 重复(同一项目只能有一个当前版本)`)
          }
          variants.set(item.variant, file)
          if (!file.startsWith(`${entry}-${item.variant}-`)) {
            fail(at, `文件名里应包含 variant:"${entry}-${item.variant}-…"`)
          }
        }
      }

      if (item.version !== undefined) {
        if (typeof item.version !== 'string' || !VERSION_RE.test(item.version)) {
          fail(at, `version 必须是 X.Y.Z:${item.version}`)
        } else if (!file.includes(`-v${item.version}-`)) {
          fail(at, `文件名里应包含 -v${item.version}-`)
        }
      }

      if (item.channel !== undefined && !['stable', 'beta'].includes(item.channel)) {
        fail(at, `channel 只能是 stable / beta(当前 ${item.channel})`)
      }
      if (item.notes !== undefined) checkLocalized(at, item.notes, 'notes')
    }

    // 目录里的 .fwpkg 必须全部登记
    for (const name of readdirSync(projectDir)) {
      const full = join(projectDir, name)
      if (!statSync(full).isDirectory() && name.endsWith('.fwpkg') && !listed.has(name)) {
        fail(where, `有未登记的固件:${name}`)
      }
    }

    if (dates.length > 0 && typeof manifest.updated === 'string') {
      const latest = dates.slice().sort().at(-1)
      if (manifest.updated !== latest) {
        fail(where, `updated "${manifest.updated}" 必须等于最晚的文件日期 "${latest}"`)
      }
    }

    if (errors.length === errorCountBefore) {
      console.log(`✓ ${where}  ${manifest.firmware.length} 个文件`)
    }
  }
}

for (const message of warnings) console.log(`⚠ ${message}`)
if (errors.length > 0) {
  console.error(`\n✗ 校验失败:${errors.length} 个问题`)
  for (const message of errors) console.error(`  ${message}`)
  process.exitCode = 1
} else {
  console.log(`\n✓ 校验通过:${projects} 个项目 · ${files} 个固件`)
}