/**
 * System status page query helpers — system-status-page.
 *
 * Exports:
 *  - deriveServiceStatuses / getActiveIncidents / getIncidentHistory  (reads)
 *  - createIncident / addIncidentUpdate / resolveIncident             (writes)
 *  - addSubscriber / listSubscribers / getSubscriberByToken / removeSubscriber
 *  - serializeIncident                                                (serialiser)
 */
import { eq, isNull, gte, desc, and, sql, inArray } from 'drizzle-orm'
import type { Db } from '../client'
import { systemIncidents, systemIncidentUpdates, statusSubscribers } from '../schema/status'
import { auditLog } from './_audit-forward'

// Sentinel UUID for system-level (cross-tenant) audit rows
const SYSTEM_TENANT_ID = '00000000-0000-0000-0000-000000000000'
import type {
  StatusServiceId,
  ServiceStatus,
  IncidentStatus,
  IncidentImpact,
  IncidentObject,
  IncidentUpdateObject,
  ServiceStatusObject,
  StatusHistoryDay,
  StatusHistoryPayload,
} from '@zync/types'
import { STATUS_SERVICES as SERVICES } from '@zync/types'

// ── Serialisation ─────────────────────────────────────────────────────────────

export function serializeIncidentUpdate(
  row: typeof systemIncidentUpdates.$inferSelect,
): IncidentUpdateObject {
  return {
    id: row.id,
    body: row.body,
    status: row.status as IncidentStatus,
    createdAt: row.createdAt.toISOString(),
  }
}

export function serializeIncident(
  row: typeof systemIncidents.$inferSelect,
  updates: typeof systemIncidentUpdates.$inferSelect[],
): IncidentObject {
  return {
    id: row.id,
    title: row.title,
    status: row.status as IncidentStatus,
    impact: row.impact as IncidentImpact,
    affectedServices: (row.affectedServices ?? []) as StatusServiceId[],
    createdAt: row.createdAt.toISOString(),
    resolvedAt: row.resolvedAt ? row.resolvedAt.toISOString() : null,
    updates: updates.map(serializeIncidentUpdate),
  }
}

// ── Derivation ────────────────────────────────────────────────────────────────

/**
 * Compute per-service status from the set of active (unresolved) incidents.
 * A service is:
 *   - 'outage'     if any active incident listing it has impact='critical'
 *   - 'degraded'   if any active incident listing it has impact='minor'|'major'
 *   - 'operational' otherwise
 */
export function deriveServiceStatuses(active: IncidentObject[]): ServiceStatusObject[] {
  const worstMap = new Map<StatusServiceId, ServiceStatus>()

  for (const incident of active) {
    const serviceStatus: ServiceStatus =
      incident.impact === 'critical' ? 'outage' : 'degraded'
    for (const svcId of incident.affectedServices) {
      const current = worstMap.get(svcId) ?? 'operational'
      // outage > degraded > operational
      if (serviceStatus === 'outage' || current === 'operational') {
        worstMap.set(svcId, serviceStatus)
      }
    }
  }

  return SERVICES.map((svc) => ({
    id: svc.id,
    label: svc.label,
    status: worstMap.get(svc.id) ?? 'operational',
  }))
}

/**
 * Overall status = worst of all per-service statuses.
 */
export function deriveOverallStatus(services: ServiceStatusObject[]): ServiceStatus {
  if (services.some((s) => s.status === 'outage')) return 'outage'
  if (services.some((s) => s.status === 'degraded')) return 'degraded'
  return 'operational'
}

// ── Reads ─────────────────────────────────────────────────────────────────────

/**
 * Fetch all active (unresolved) incidents with their updates, updates sorted
 * newest first.
 */
export async function getActiveIncidents(db: Db): Promise<IncidentObject[]> {
  const incidents = await db
    .select()
    .from(systemIncidents)
    .where(isNull(systemIncidents.resolvedAt))
    .orderBy(desc(systemIncidents.createdAt))

  if (incidents.length === 0) return []

  const incidentIds = incidents.map((i) => i.id)
  const updates = await db
    .select()
    .from(systemIncidentUpdates)
    .where(inArray(systemIncidentUpdates.incidentId, incidentIds))
    .orderBy(desc(systemIncidentUpdates.createdAt))

  return incidents.map((inc) =>
    serializeIncident(
      inc,
      updates.filter((u) => u.incidentId === inc.id),
    ),
  )
}

/**
 * Fetch incidents from the past `days` days (resolved and unresolved) with
 * their updates; compute per-day worst status and uptimePct.
 */
export async function getIncidentHistory(
  db: Db,
  days: number,
): Promise<StatusHistoryPayload> {
  const since = new Date(Date.now() - days * 24 * 60 * 60 * 1000)

  const incidents = await db
    .select()
    .from(systemIncidents)
    .where(gte(systemIncidents.createdAt, since))
    .orderBy(desc(systemIncidents.createdAt))

  const incidentIds = incidents.map((i) => i.id)

  let updates: typeof systemIncidentUpdates.$inferSelect[] = []
  if (incidentIds.length > 0) {
    updates = await db
      .select()
      .from(systemIncidentUpdates)
      .where(inArray(systemIncidentUpdates.incidentId, incidentIds))
      .orderBy(desc(systemIncidentUpdates.createdAt))
  }

  const serialized = incidents.map((inc) =>
    serializeIncident(
      inc,
      updates.filter((u) => u.incidentId === inc.id),
    ),
  )

  // Build per-day worst status
  const dayMap = new Map<string, ServiceStatus>()
  for (const inc of serialized) {
    // span: from createdAt to resolvedAt (or "today")
    const start = new Date(inc.createdAt)
    const end = inc.resolvedAt ? new Date(inc.resolvedAt) : new Date()
    const incStatus: ServiceStatus =
      inc.impact === 'critical' ? 'outage' : 'degraded'

    // iterate days spanned
    const cursor = new Date(start)
    cursor.setUTCHours(0, 0, 0, 0)
    while (cursor <= end) {
      const key = cursor.toISOString().slice(0, 10)
      const current = dayMap.get(key) ?? 'operational'
      if (incStatus === 'outage' || current === 'operational') {
        dayMap.set(key, incStatus)
      }
      cursor.setUTCDate(cursor.getUTCDate() + 1)
    }
  }

  // Fill all days in range
  const dayEntries: StatusHistoryDay[] = []
  const cursor = new Date(since)
  cursor.setUTCHours(0, 0, 0, 0)
  const today = new Date()
  today.setUTCHours(0, 0, 0, 0)
  while (cursor <= today) {
    const key = cursor.toISOString().slice(0, 10)
    dayEntries.push({ date: key, status: dayMap.get(key) ?? 'operational' })
    cursor.setUTCDate(cursor.getUTCDate() + 1)
  }

  const operationalDays = dayEntries.filter((d) => d.status === 'operational').length
  const uptimePct =
    dayEntries.length > 0
      ? Math.round((operationalDays / dayEntries.length) * 1000) / 10
      : 100

  return { days: dayEntries, uptimePct, incidents: serialized }
}

// ── Writes ────────────────────────────────────────────────────────────────────

export async function createIncident(
  db: Db,
  input: {
    title: string
    status: IncidentStatus
    impact: IncidentImpact
    affectedServices: StatusServiceId[]
    body: string
    createdBy: string
  },
): Promise<IncidentObject> {
  return db.transaction(async (tx) => {
    const incRows = await tx
      .insert(systemIncidents)
      .values({
        title: input.title,
        status: input.status,
        impact: input.impact,
        affectedServices: input.affectedServices,
        createdBy: input.createdBy,
      })
      .returning()
    const inc = incRows[0]
    if (!inc) throw new Error('createIncident: insert returned no rows')

    const updateRows = await tx
      .insert(systemIncidentUpdates)
      .values({
        incidentId: inc.id,
        body: input.body,
        status: input.status,
        createdBy: input.createdBy,
      })
      .returning()

    await tx.insert(auditLog).values({
      tenantId: SYSTEM_TENANT_ID,
      actorId: input.createdBy,
      actorType: 'user',
      entityType: 'system_incident',
      entityId: inc.id,
      action: 'incident.created',
    })

    return serializeIncident(inc, updateRows)
  })
}

export async function addIncidentUpdate(
  db: Db,
  incidentId: string,
  input: {
    body: string
    status: IncidentStatus
    createdBy: string
  },
): Promise<IncidentObject> {
  return db.transaction(async (tx) => {
    // Update incident status
    const [inc] = await tx
      .update(systemIncidents)
      .set({ status: input.status })
      .where(eq(systemIncidents.id, incidentId))
      .returning()

    if (!inc) throw new IncidentNotFoundError(incidentId)

    await tx
      .insert(systemIncidentUpdates)
      .values({
        incidentId,
        body: input.body,
        status: input.status,
        createdBy: input.createdBy,
      })

    const allUpdates = await tx
      .select()
      .from(systemIncidentUpdates)
      .where(eq(systemIncidentUpdates.incidentId, incidentId))
      .orderBy(desc(systemIncidentUpdates.createdAt))

    await tx.insert(auditLog).values({
      tenantId: SYSTEM_TENANT_ID,
      actorId: input.createdBy,
      actorType: 'user',
      entityType: 'system_incident',
      entityId: incidentId,
      action: 'incident.updated',
    })

    return serializeIncident(inc, allUpdates)
  })
}

export async function resolveIncident(
  db: Db,
  incidentId: string,
  input: {
    body: string
    createdBy: string
  },
): Promise<IncidentObject> {
  return db.transaction(async (tx) => {
    const [inc] = await tx
      .update(systemIncidents)
      .set({ status: 'resolved', resolvedAt: new Date() })
      .where(and(eq(systemIncidents.id, incidentId), isNull(systemIncidents.resolvedAt)))
      .returning()

    if (!inc) throw new IncidentAlreadyResolvedError(incidentId)

    await tx
      .insert(systemIncidentUpdates)
      .values({
        incidentId,
        body: input.body,
        status: 'resolved',
        createdBy: input.createdBy,
      })

    const allUpdates = await tx
      .select()
      .from(systemIncidentUpdates)
      .where(eq(systemIncidentUpdates.incidentId, incidentId))
      .orderBy(desc(systemIncidentUpdates.createdAt))

    await tx.insert(auditLog).values({
      tenantId: SYSTEM_TENANT_ID,
      actorId: input.createdBy,
      actorType: 'user',
      entityType: 'system_incident',
      entityId: incidentId,
      action: 'incident.resolved',
    })

    return serializeIncident(inc, allUpdates)
  })
}

// ── Subscriber helpers ────────────────────────────────────────────────────────

/**
 * Idempotent subscribe — inserts or ignores if email already exists.
 * Returns the unsubscribeToken on new insert, null if already subscribed.
 */
export async function addSubscriber(
  db: Db,
  email: string,
): Promise<{ unsubscribeToken: string } | null> {
  // The default for unsubscribe_token is set in raw DDL via pgcrypto.
  // Drizzle doesn't model gen_random_bytes, so we supply a JS-generated token
  // as a fallback while the real default handles it at the DB level.
  const token = crypto.randomUUID().replace(/-/g, '') + crypto.randomUUID().replace(/-/g, '')
  const rows = await db
    .insert(statusSubscribers)
    .values({ email, unsubscribeToken: token })
    .onConflictDoNothing()
    .returning()

  if (rows.length === 0) return null
  return { unsubscribeToken: rows[0]!.unsubscribeToken }
}

export async function listSubscribers(
  db: Db,
): Promise<typeof statusSubscribers.$inferSelect[]> {
  return db.select().from(statusSubscribers)
}

export async function getSubscriberByToken(
  db: Db,
  token: string,
): Promise<typeof statusSubscribers.$inferSelect | null> {
  const [row] = await db
    .select()
    .from(statusSubscribers)
    .where(eq(statusSubscribers.unsubscribeToken, token))
    .limit(1)
  return row ?? null
}

export async function removeSubscriber(db: Db, token: string): Promise<boolean> {
  const deleted = await db
    .delete(statusSubscribers)
    .where(eq(statusSubscribers.unsubscribeToken, token))
    .returning()
  return deleted.length > 0
}

// ── Errors ────────────────────────────────────────────────────────────────────

export class IncidentNotFoundError extends Error {
  constructor(id: string) {
    super(`Incident not found: ${id}`)
    this.name = 'IncidentNotFoundError'
  }
}

export class IncidentAlreadyResolvedError extends Error {
  constructor(id: string) {
    super(`Incident is already resolved: ${id}`)
    this.name = 'IncidentAlreadyResolvedError'
  }
}
