import { Hono, type Context } from 'hono'
import type { JwtContext } from '../middleware/authJwt.js'
import { authJwtOrApiKey } from '../middleware/authJwtOrApiKey.js'
import { requireRole } from '../middleware/requireRole.js'
import { detectAnomalies, fetchContributingFactors } from '../lib/anomaly.js'
import { getAnomalyHistory } from '../lib/anomaly-snapshot.js'
import { supabaseAdmin } from '../lib/db.js'
import { parsePositiveFloat, parseClampedFloat } from '../lib/params.js'
import { ApiError } from '../lib/errors.js'

const requireEdit = requireRole('admin', 'editor')

/**
 * Anomaly endpoints.
 *
 *   GET    /api/v1/anomalies              live detection (with ack state)
 *   GET    /api/v1/anomalies/history      persisted history from cron snapshots
 *   POST   /api/v1/anomalies/ack          acknowledge one (provider/model/kind)
 *   DELETE /api/v1/anomalies/ack          un-acknowledge
 */

export const anomaliesRouter = new Hono<JwtContext>()

anomaliesRouter.use('*', authJwtOrApiKey)

const VALID_KINDS = new Set(['latency', 'cost', 'error_rate'])


interface AckKey {
  provider: string
  model: string
  kind: string
}

function ackKey(a: AckKey): string {
  return `${a.provider}|${a.model}|${a.kind}`
}

anomaliesRouter.get('/', async (c) => {
  const orgId = c.get('orgId')
  if (!orgId) throw new ApiError('NOT_FOUND', 'Organization not found')

  const observationHours = parseClampedFloat(c.req.query('observationHours'), 1, 0.25, 72)
  const referenceHours = parseClampedFloat(c.req.query('referenceHours'), 168, 1, 8760)
  const sigmaThreshold = parseClampedFloat(c.req.query('sigma'), 3, 1, 10)
  const projectId = c.req.query('projectId')

  const now = Date.now()
  const obsStart = new Date(now - observationHours * 3_600_000).toISOString()
  const refStart = new Date(now - referenceHours * 3_600_000).toISOString()

  if (projectId) {
    const { data: proj } = await supabaseAdmin
      .from('projects')
      .select('id')
      .eq('id', projectId)
      .eq('organization_id', orgId)
      .single()
    if (!proj) throw new ApiError('NOT_FOUND', 'Project not found')
  }

  // Fetch acks scoped to the same project context (NULL = org-wide).
  let acksQuery = supabaseAdmin
    .from('anomaly_acks')
    .select('provider, model, kind, acknowledged_at')
    .eq('organization_id', orgId)
    .limit(500)

  if (projectId) {
    acksQuery = acksQuery.eq('project_id', projectId)
  } else {
    acksQuery = acksQuery.is('project_id', null)
  }

  const [anomalies, acksRes] = await Promise.all([
    detectAnomalies(orgId, {
      observationHours,
      referenceHours,
      sigmaThreshold,
      ...(projectId ? { projectId } : {}),
    }),
    acksQuery,
  ])

  const ackMap = new Map<string, string>()
  for (const row of acksRes.data ?? []) {
    ackMap.set(ackKey(row as AckKey), (row as { acknowledged_at: string }).acknowledged_at)
  }

  // Deduplicate by (provider, model) to avoid redundant DB calls when both
  // cost and latency anomalies exist for the same model.
  const uniqueModels = [...new Map(anomalies.map((a) => [`${a.provider}|${a.model}`, a])).values()]
  const factorsList = await Promise.all(
    uniqueModels.map((a) =>
      fetchContributingFactors(orgId, a.provider, a.model, obsStart, refStart, projectId),
    ),
  )
  const factorsMap = new Map(
    uniqueModels.map((a, i) => [`${a.provider}|${a.model}`, factorsList[i]]),
  )

  const withAcks = anomalies.map((a) => ({
    ...a,
    acknowledgedAt: ackMap.get(ackKey(a)) ?? null,
    factors: factorsMap.get(`${a.provider}|${a.model}`) ?? null,
  }))

  return c.json({
    success: true,
    data: withAcks,
    meta: {
      observationHours,
      referenceHours,
      sigmaThreshold,
      count: withAcks.length,
    },
  })
})

// GET /api/v1/anomalies/history?days=30
anomaliesRouter.get('/history', async (c) => {
  const orgId = c.get('orgId')
  if (!orgId) throw new ApiError('NOT_FOUND', 'Organization not found')

  const days = Math.min(parsePositiveFloat(c.req.query('days'), 30), 365)
  const history = await getAnomalyHistory(orgId, days)

  return c.json({
    success: true,
    data: history,
    meta: { days, count: history.length },
  })
})

async function parseAckBody(
  c: Context<JwtContext>,
): Promise<{ error: string } | { provider: string; model: string; kind: string; projectId?: string }> {
  let body: { provider?: unknown; model?: unknown; kind?: unknown; projectId?: unknown }
  try {
    body = (await c.req.json()) as typeof body
  } catch {
    return { error: 'Invalid JSON body' }
  }
  if (typeof body.provider !== 'string' || body.provider.trim().length === 0) {
    return { error: 'provider is required' }
  }
  if (typeof body.model !== 'string' || body.model.trim().length === 0) {
    return { error: 'model is required' }
  }
  if (typeof body.kind !== 'string' || !VALID_KINDS.has(body.kind)) {
    return { error: 'kind must be one of: latency, cost, error_rate' }
  }
  const projectId = typeof body.projectId === 'string' && body.projectId.length > 0
    ? body.projectId
    : undefined
  return {
    provider: body.provider,
    model: body.model,
    kind: body.kind,
    ...(projectId !== undefined ? { projectId } : {}),
  }
}

// POST /api/v1/anomalies/ack — acknowledge (upsert)
anomaliesRouter.post('/ack', requireEdit, async (c) => {
  const orgId = c.get('orgId')
  const userId = c.get('userId')
  if (!orgId) throw new ApiError('NOT_FOUND', 'Organization not found')

  const parsed = await parseAckBody(c)
  if ('error' in parsed) throw new ApiError('VALIDATION_FAILED', parsed.error)

  if (parsed.projectId) {
    const { data: proj } = await supabaseAdmin
      .from('projects')
      .select('id')
      .eq('id', parsed.projectId)
      .eq('organization_id', orgId)
      .single()
    if (!proj) throw new ApiError('NOT_FOUND', 'Project not found')
  }

  const { error } = await supabaseAdmin
    .from('anomaly_acks')
    .upsert({
      organization_id: orgId,
      project_id: parsed.projectId ?? null,
      provider: parsed.provider,
      model: parsed.model,
      kind: parsed.kind,
      acknowledged_by: userId ?? null,
      acknowledged_at: new Date().toISOString(),
    }, { onConflict: 'organization_id,project_id,provider,model,kind' })

  if (error) throw new ApiError('INTERNAL_ERROR', 'Failed to acknowledge anomaly')

  return c.json({ success: true })
})

// DELETE /api/v1/anomalies/ack?provider=X&model=Y&kind=Z[&projectId=P] — un-acknowledge
anomaliesRouter.delete('/ack', requireEdit, async (c) => {
  const orgId = c.get('orgId')
  if (!orgId) throw new ApiError('NOT_FOUND', 'Organization not found')

  const provider = c.req.query('provider')
  const model = c.req.query('model')
  const kind = c.req.query('kind')
  const projectId = c.req.query('projectId')

  if (!provider) throw new ApiError('VALIDATION_FAILED', 'provider is required')
  if (!model) throw new ApiError('VALIDATION_FAILED', 'model is required')
  if (!kind || !VALID_KINDS.has(kind)) {
    throw new ApiError('VALIDATION_FAILED', 'kind must be one of: latency, cost, error_rate')
  }

  if (projectId) {
    const { data: proj } = await supabaseAdmin
      .from('projects')
      .select('id')
      .eq('id', projectId)
      .eq('organization_id', orgId)
      .single()
    if (!proj) throw new ApiError('NOT_FOUND', 'Project not found')
  }

  let deleteQuery = supabaseAdmin
    .from('anomaly_acks')
    .delete()
    .eq('organization_id', orgId)
    .eq('provider', provider)
    .eq('model', model)
    .eq('kind', kind)

  if (projectId) {
    deleteQuery = deleteQuery.eq('project_id', projectId)
  } else {
    deleteQuery = deleteQuery.is('project_id', null)
  }

  const { error } = await deleteQuery

  if (error) throw new ApiError('INTERNAL_ERROR', 'Failed to un-acknowledge')

  return c.json({ success: true })
})
