// Wekelijks handmatig sync-script voor de model_prices tabel.
//
// Gebruik:
//   npm run db:sync-model-prices              # echt synchroniseren
//   npm run db:sync-model-prices -- --dry-run # tonen, niets schrijven
//
// Anthropic biedt geen prijs-API. /v1/models levert alleen modellijst +
// metadata. De prijzen onderhouden we daarom in PRICE_TABLE hieronder.
// De API-call dient om de modellijst te valideren en nieuwe modellen op te
// merken (warning) zodat we weten dat PRICE_TABLE een update nodig heeft.
//
// Plan: docs/plans/sync-model-prices.md

import { PrismaClient } from '@prisma/client'
import * as dotenv from 'dotenv'
import * as path from 'path'
import { Pool } from 'pg'
import { PrismaPg } from '@prisma/adapter-pg'

const root = path.resolve(__dirname, '..')
dotenv.config({ path: path.join(root, '.env.local'), override: true })
dotenv.config({ path: path.join(root, '.env') })

const ANTHROPIC_API_BASE = 'https://api.anthropic.com'
const ANTHROPIC_API_VERSION = '2023-06-01'

// Prijzen per 1M tokens in USD. Bij elke prijswijziging hier updaten.
// Bron: https://platform.claude.com/docs/en/about-claude/pricing
const PRICE_TABLE: Record<string, { input: number; output: number }> = {
  'claude-opus-4-7': { input: 15.0, output: 75.0 },
  'claude-sonnet-4-6': { input: 3.0, output: 15.0 },
  'claude-haiku-4-5-20251001': { input: 0.8, output: 4.0 },
}

// Cache-tier multipliers t.o.v. input-prijs (Anthropic standaarden, mei 2026):
//   cache hit (read)    = 0.1× input
//   cache write 5-minute = 1.25× input  (dit veld in onze DB)
//   cache write 1-hour  = 2.0× input    (niet apart opgeslagen)
const CACHE_READ_RATIO = 0.1
const CACHE_WRITE_RATIO = 1.25

// Alleen Claude 4.x synchroniseren — oudere 3.x worden overgeslagen.
const CLAUDE_4X_REGEX = /^claude-(opus|sonnet|haiku)-4/

interface AnthropicModel {
  id: string
  type: string
  display_name: string
  created_at: string
}

interface AnthropicModelsResponse {
  data: AnthropicModel[]
  has_more: boolean
  last_id: string | null
}

interface Args {
  dryRun: boolean
}

function parseArgs(argv: string[]): Args {
  let dryRun = false
  for (const a of argv) {
    if (a === '--dry-run') dryRun = true
    else if (a.startsWith('--')) throw new Error(`Unknown flag: ${a}`)
    else throw new Error(`Unexpected argument: ${a}`)
  }
  return { dryRun }
}

async function sleep(ms: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, ms))
}

async function fetchModelsPage(
  apiKey: string,
  afterId: string | null,
): Promise<AnthropicModelsResponse> {
  const url = new URL(`${ANTHROPIC_API_BASE}/v1/models`)
  url.searchParams.set('limit', '1000')
  if (afterId) url.searchParams.set('after_id', afterId)

  const headers = {
    'x-api-key': apiKey,
    'anthropic-version': ANTHROPIC_API_VERSION,
  }

  let lastError: unknown = null
  for (let attempt = 0; attempt < 2; attempt++) {
    try {
      const res = await fetch(url, { headers })
      if (res.status === 401) {
        throw new Error(
          'Anthropic API gaf 401 Unauthorized. Controleer ANTHROPIC_API_KEY in .env.local.',
        )
      }
      if (res.status >= 500) {
        lastError = new Error(`Anthropic API gaf ${res.status} ${res.statusText}`)
        if (attempt === 0) {
          console.warn(`  ⚠ ${(lastError as Error).message} — retry over 2s...`)
          await sleep(2000)
          continue
        }
        throw lastError
      }
      if (!res.ok) {
        const body = await res.text()
        throw new Error(`Anthropic API gaf ${res.status} ${res.statusText}: ${body.slice(0, 300)}`)
      }
      return (await res.json()) as AnthropicModelsResponse
    } catch (err) {
      if (err instanceof TypeError) {
        // Network/DNS error
        lastError = err
        if (attempt === 0) {
          console.warn(`  ⚠ Netwerkfout: ${err.message} — retry over 2s...`)
          await sleep(2000)
          continue
        }
      }
      throw err
    }
  }
  throw lastError ?? new Error('Unbekende fout bij ophalen /v1/models')
}

async function fetchAllClaude4xModels(apiKey: string): Promise<AnthropicModel[]> {
  const all: AnthropicModel[] = []
  let afterId: string | null = null
  let totalReceived = 0

  while (true) {
    const page = await fetchModelsPage(apiKey, afterId)
    totalReceived += page.data.length
    for (const m of page.data) {
      if (CLAUDE_4X_REGEX.test(m.id)) all.push(m)
    }
    if (!page.has_more || !page.last_id) break
    afterId = page.last_id
  }

  console.log(`  → ${totalReceived} models received, ${all.length} match Claude 4.x filter`)
  return all
}

interface SyncResult {
  created: number
  updated: number
  unchanged: number
  skipped: number
}

async function syncModel(
  prisma: PrismaClient,
  modelId: string,
  price: { input: number; output: number },
  dryRun: boolean,
): Promise<'created' | 'updated' | 'unchanged'> {
  const cacheRead = round6(price.input * CACHE_READ_RATIO)
  const cacheWrite = round6(price.input * CACHE_WRITE_RATIO)

  const data = {
    model_id: modelId,
    input_price_per_1m: price.input,
    output_price_per_1m: price.output,
    cache_read_price_per_1m: cacheRead,
    cache_write_price_per_1m: cacheWrite,
  }

  const existing = await prisma.modelPrice.findUnique({
    where: { model_id: modelId },
    select: {
      input_price_per_1m: true,
      output_price_per_1m: true,
      cache_read_price_per_1m: true,
      cache_write_price_per_1m: true,
    },
  })

  if (!existing) {
    if (!dryRun) {
      await prisma.modelPrice.create({ data })
    }
    return 'created'
  }

  const same =
    Number(existing.input_price_per_1m) === data.input_price_per_1m &&
    Number(existing.output_price_per_1m) === data.output_price_per_1m &&
    Number(existing.cache_read_price_per_1m) === data.cache_read_price_per_1m &&
    Number(existing.cache_write_price_per_1m) === data.cache_write_price_per_1m

  if (same) return 'unchanged'

  if (!dryRun) {
    await prisma.modelPrice.update({ where: { model_id: modelId }, data })
  }
  return 'updated'
}

function round6(n: number): number {
  return Math.round(n * 1_000_000) / 1_000_000
}

async function main(): Promise<void> {
  const args = parseArgs(process.argv.slice(2))

  const apiKey = process.env.ANTHROPIC_API_KEY
  if (!apiKey) {
    throw new Error(
      'ANTHROPIC_API_KEY is not set. Voeg toe aan .env.local — zie console.anthropic.com → API Keys.',
    )
  }

  const dbUrl = process.env.DATABASE_URL
  if (!dbUrl) throw new Error('DATABASE_URL is not set. Check .env.local')

  const pool = new Pool({ connectionString: dbUrl })
  const adapter = new PrismaPg(pool)
  const prisma = new PrismaClient({ adapter })

  try {
    console.log(`Fetching /v1/models from Anthropic API...${args.dryRun ? ' [DRY RUN]' : ''}`)
    const models = await fetchAllClaude4xModels(apiKey)

    if (models.length === 0) {
      console.error('  ✗ Geen Claude 4.x modellen ontvangen — aborting.')
      process.exit(1)
    }

    console.log('\nSyncing prices:')
    const result: SyncResult = { created: 0, updated: 0, unchanged: 0, skipped: 0 }

    const apiIds = new Set<string>()
    for (const m of models) {
      apiIds.add(m.id)
      const price = PRICE_TABLE[m.id]
      if (!price) {
        console.warn(
          `  ⚠ ${m.id.padEnd(36)} (geen prijs in PRICE_TABLE — voeg toe aan scripts/sync-model-prices.ts)`,
        )
        result.skipped++
        continue
      }
      const action = await syncModel(prisma, m.id, price, args.dryRun)
      const tag = action === 'created' ? '✓' : action === 'updated' ? '✓' : '·'
      console.log(`  ${tag} ${m.id.padEnd(36)} (${action})`)
      result[action]++
    }

    // Detect models in DB that no longer appear in the API (don't delete!).
    const dbModels = await prisma.modelPrice.findMany({ select: { model_id: true } })
    const orphaned = dbModels.filter((m) => !apiIds.has(m.model_id))
    if (orphaned.length > 0) {
      console.log('\nDB-modellen die niet (meer) in /v1/models staan (worden NIET verwijderd):')
      for (const o of orphaned) console.log(`  · ${o.model_id}`)
    }

    console.log(
      `\nResult: ${result.created} created, ${result.updated} updated, ${result.unchanged} unchanged, ${result.skipped} skipped${args.dryRun ? ' [DRY RUN — niets geschreven]' : ''}`,
    )
  } finally {
    await prisma.$disconnect()
    await pool.end()
  }
}

main().catch((err) => {
  console.error('\nsync-model-prices failed:', err.message)
  process.exit(1)
})
