import crypto from 'node:crypto'
import Fastify from 'fastify'
import { z } from 'zod'
import {
  getAttribute, qualityIssueTypes, getCategory, getInventory, getPricing, getProduct, listCategories, listStores, ping, qualityIssues,
  qualitySummary, resolveStore, searchProducts, syncStatus, visibilityFacts, type Db,
} from '@znet/catalog-db'
import { PublicError, type Logger } from '@znet/shared'
import { diagnoseVisibility } from './visibility.js'

export interface AppOptions {
  db: Db
  token: string
  defaultStore: string
  logger: Logger
  /** false when stock is not managed in Magento. */
  stockEnabled?: boolean
}

const storeQuery = z.object({ store: z.string().min(1).max(64).optional() })
const page = { limit: z.coerce.number().int().min(1).max(100).default(20), offset: z.coerce.number().int().min(0).max(100_000).default(0) }

function tokensMatch(given: string, expected: string) {
  const a = Buffer.from(given)
  const b = Buffer.from(expected)
  return a.length === b.length && crypto.timingSafeEqual(a, b)
}

/** Internal Catalog API — the single reader of the catalog schema. */
export function buildApp({ db, token, defaultStore, logger, stockEnabled = true }: AppOptions) {
  const issueTypes = qualityIssueTypes({ stock: stockEnabled })
  const app = Fastify({ loggerInstance: logger, trustProxy: false })

  app.addHook('onRequest', async (req, reply) => {
    if (req.url === '/health') return
    const header = req.headers.authorization ?? ''
    const given = header.startsWith('Bearer ') ? header.slice(7) : ''
    if (!tokensMatch(given, token)) return reply.code(401).send({ error: 'unauthorized' })
  })

  app.setErrorHandler((err, req, reply) => {
    if (err instanceof z.ZodError) return reply.code(400).send({ error: 'bad_request', issues: err.issues.map(i => ({ path: i.path.join('.'), message: i.message })) })
    if (err instanceof PublicError) return reply.code(err.status).send({ error: err.code, message: err.message })
    req.log.error({ err }, 'unhandled error')
    return reply.code(500).send({ error: 'internal_error' })
  })

  const store = (q: unknown) => resolveStore(db, storeQuery.parse(q).store ?? defaultStore)

  app.get('/health', async () => {
    await ping(db)
    return { ok: true }
  })

  app.get('/stores', async () => ({ items: await listStores(db) }))

  app.get('/products', async req => {
    const q = z
      .object({
        q: z.string().trim().max(200).optional(),
        category: z.coerce.number().int().positive().optional(),
        type: z.string().max(32).optional(),
        status: z.enum(['enabled', 'disabled']).optional(),
        in_stock: z.enum(['true', 'false']).transform(v => v === 'true').optional(),
        ...page,
      })
      .parse(req.query)
    return searchProducts(db, await store(req.query), {
      q: q.q || undefined, categoryId: q.category, typeId: q.type, status: q.status, inStock: q.in_stock, limit: q.limit, offset: q.offset,
    })
  })

  const skuParam = z.object({ sku: z.string().min(1).max(128) })

  app.get('/products/:sku', async req => getProduct(db, await store(req.query), skuParam.parse(req.params).sku))

  app.get('/products/:sku/visibility', async req =>
    diagnoseVisibility(await visibilityFacts(db, await store(req.query), skuParam.parse(req.params).sku), { stock: stockEnabled }),
  )

  app.get('/prices/:sku', async req => getPricing(db, await store(req.query), skuParam.parse(req.params).sku))

  app.get('/inventory/:sku', async req => getInventory(db, skuParam.parse(req.params).sku))

  app.get('/categories', async req => {
    const { parent } = z.object({ parent: z.coerce.number().int().min(0).optional() }).parse(req.query)
    return { items: await listCategories(db, await store(req.query), parent) }
  })

  app.get('/categories/:id', async req => {
    const { id } = z.object({ id: z.coerce.number().int().positive() }).parse(req.params)
    return getCategory(db, await store(req.query), id)
  })

  app.get('/attributes/:code', async req => getAttribute(db, z.object({ code: z.string().min(1).max(128) }).parse(req.params).code))

  app.get('/quality/summary', async req => qualitySummary(db, (await store(req.query)).storeId, issueTypes))

  app.get('/quality/issues', async req => {
    const q = z.object({ type: z.enum(issueTypes as [string, ...string[]]), ...page }).parse(req.query)
    return qualityIssues(db, q.type as (typeof issueTypes)[number], (await store(req.query)).storeId, q.limit, q.offset)
  })

  app.get('/sync/status', async () => syncStatus(db))

  return app
}
