export const CI_DELIVERY_PANEL_ID = 'deploy-status' as const

export type CiDeliveryStatus = 'idle' | 'queued' | 'running' | 'stalled' | 'unknown'
export type CiDeliveryCompleteness = 'complete' | 'incomplete'
export type CiDeliveryFreshness = 'fresh' | 'stale' | 'unknown'

export interface CiDeliveryRequest {
  id: string
  observedAt: string
}

export interface CiDeliveryQueue {
  depth: number
  oldestAgeMs: number | null
  requests: CiDeliveryRequest[]
}

export interface CiDeliveryOperation {
  operation: string | null
  holder: string | null
}

export interface CiDeliveryEvent {
  at: string
  type: string
  detail: string | null
}

export interface CiDeliveryIdentity {
  servedSha: string | null
  mainSha: string | null
  commitsBehind: number | null
  mainCommitAt: string | null
}

/** S5: the S3 controller watcher's own retry-stop record, read from its `/status`. */
export interface CiDeliveryWatcher {
  targetSha: string
  attempts: number
  lastStatus: string
  lastDetail: string
  lastAt: string
  lastOk: boolean
  failureClass: 'none' | 'transient' | 'permanent'
  nextRetryAt: string | null
}

export interface CiDeliveryData {
  status: CiDeliveryStatus
  completeness: CiDeliveryCompleteness
  freshness: CiDeliveryFreshness
  observedAt: string | null
  queue: CiDeliveryQueue | null
  current: CiDeliveryOperation | null
  latestEvent: CiDeliveryEvent | null
  latestProgressAt: string | null
  reason: string | null
  identity: CiDeliveryIdentity
  watcher: CiDeliveryWatcher | null
}

const UNKNOWN_IDENTITY: CiDeliveryIdentity = { servedSha: null, mainSha: null, commitsBehind: null, mainCommitAt: null }

const UNKNOWN_DELIVERY_DATA: CiDeliveryData = {
  status: 'unknown',
  completeness: 'incomplete',
  freshness: 'unknown',
  observedAt: null,
  queue: null,
  current: null,
  latestEvent: null,
  latestProgressAt: null,
  reason: 'deploy-status data unavailable',
  identity: UNKNOWN_IDENTITY,
  watcher: null,
}

function unknown(reason: string): CiDeliveryData {
  return { ...UNKNOWN_DELIVERY_DATA, reason }
}

function record(value: unknown): Record<string, unknown> | null {
  return value !== null && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : null
}

function text(value: unknown, field: string, nullable = false): string | null {
  if (nullable && value === null) return null
  if (typeof value === 'string' && value.trim().length > 0) return value
  throw new TypeError(`deploy-status ${field} must be a non-empty string${nullable ? ' or null' : ''}`)
}

function timestamp(value: unknown, field: string, nullable = false): string | null {
  const result = text(value, field, nullable)
  if (result !== null && !Number.isFinite(Date.parse(result))) throw new TypeError(`deploy-status ${field} must be an ISO timestamp`)
  return result
}

function nonNegativeNumber(value: unknown, field: string, nullable = false): number | null {
  if (nullable && value === null) return null
  if (typeof value === 'number' && Number.isFinite(value) && value >= 0) return value
  throw new TypeError(`deploy-status ${field} must be a non-negative number${nullable ? ' or null' : ''}`)
}

const SHA_RE = /^[0-9a-f]{40}$/

function sha(value: unknown, field: string): string | null {
  // undefined (an older collector build predating this field) reads the same as null —
  // the identity panel is enabling-only and must never turn an otherwise-valid delivery
  // snapshot into a fabricated "unknown" state.
  if (value === null || value === undefined) return null
  if (typeof value === 'string' && SHA_RE.test(value)) return value
  throw new TypeError(`deploy-status ${field} must be a 40-char sha or null`)
}

function parseIdentity(source: Record<string, unknown>): CiDeliveryIdentity {
  return {
    servedSha: sha(source.servedSha, 'servedSha'),
    mainSha: sha(source.mainSha, 'mainSha'),
    commitsBehind: source.commitsBehind === undefined ? null : nonNegativeNumber(source.commitsBehind, 'commitsBehind', true),
    mainCommitAt: source.mainCommitAt === undefined ? null : timestamp(source.mainCommitAt, 'mainCommitAt', true),
  }
}

function durationLabel(ms: number): string {
  const minutes = Math.round(ms / 60_000)
  if (minutes < 1) return 'under a minute'
  if (minutes < 60) return minutes === 1 ? '1 minute' : `${minutes} minutes`
  const hours = Math.round(minutes / 60)
  if (hours < 24) return hours === 1 ? '1 hour' : `${hours} hours`
  const days = Math.round(hours / 24)
  return days === 1 ? '1 day' : `${days} days`
}

/**
 * Owner-language summary of how far the served commit trails the latest landed code —
 * a commit count alone answers "is it behind" but not "for how long", which is the
 * question the owner actually asked (the queue backed up for a day unnoticed).
 */
export function deliveryLagLabel(identity: CiDeliveryIdentity, nowMs: number = Date.now()): string {
  if (identity.servedSha === null) return 'never deployed yet'
  if (identity.mainSha === null) return "can't tell how far behind — the latest code isn't visible from here"
  if (identity.commitsBehind === null) return 'served, but the gap to the latest code is unknown'
  if (identity.commitsBehind === 0) return 'up to date'
  const count = identity.commitsBehind === 1 ? '1 change' : `${identity.commitsBehind} changes`
  if (identity.mainCommitAt === null) return `${count} behind, waiting time unknown`
  const waitedMs = nowMs - Date.parse(identity.mainCommitAt)
  if (!Number.isFinite(waitedMs) || waitedMs < 0) return `${count} behind, waiting time unknown`
  return `${count} behind, waiting ${durationLabel(waitedMs)}`
}

// Statuses deploy-local.sh/the watcher can report that mean "another deploy already
// covers this commit" — never a failure the owner needs to act on. Kept in sync with
// controller/src/deploy-watcher.ts's NON_FAILURE_STATUSES; a status outside this set
// with lastOk:false is a real, owner-actionable failure.
const WATCHER_NON_FAILURE_STATUSES = new Set(['deployed-coalesced', 'deploy-lock-timeout'])

function parseWatcher(value: unknown): CiDeliveryWatcher | null {
  if (value === null || value === undefined) return null
  const source = record(value)
  if (!source) throw new TypeError('deploy-status watcher must be an object or null')
  const lastOk = (() => {
    if (typeof source.lastOk !== 'boolean') throw new TypeError('deploy-status watcher.lastOk must be a boolean')
    return source.lastOk
  })()
  const failureClass = source.failureClass === undefined
    ? (lastOk ? 'none' : 'permanent')
    : source.failureClass
  if (failureClass !== 'none' && failureClass !== 'transient' && failureClass !== 'permanent') {
    throw new TypeError('deploy-status watcher.failureClass must be none, transient, or permanent')
  }
  return {
    targetSha: sha(source.targetSha, 'watcher.targetSha')!,
    attempts: nonNegativeNumber(source.attempts, 'watcher.attempts')!,
    lastStatus: text(source.lastStatus, 'watcher.lastStatus')!,
    lastDetail: text(source.lastDetail, 'watcher.lastDetail', true) ?? '',
    lastAt: timestamp(source.lastAt, 'watcher.lastAt')!,
    lastOk,
    failureClass,
    nextRetryAt: source.nextRetryAt === undefined ? null : timestamp(source.nextRetryAt, 'watcher.nextRetryAt', true),
  }
}

/**
 * Owner-language summary of the auto-deploy watcher's own last outcome (S5) — distinct
 * from `deliveryLagLabel`, which reports the CURRENT gap; this reports what happened the
 * last time the watcher tried to close it, including a stopped-retrying failure the lag
 * label alone would render as an indefinite "N changes behind" with no explanation.
 */
export function watcherFailureLabel(
  watcher: CiDeliveryWatcher | null,
  maxAttempts = 3,
  nowMs: number = Date.now(),
): string | null {
  if (watcher === null) return null
  if (watcher.lastOk || WATCHER_NON_FAILURE_STATUSES.has(watcher.lastStatus)) return null
  const short = watcher.targetSha.slice(0, 7)
  const detail = watcher.lastDetail ? `: ${watcher.lastDetail}` : ''
  const outcome = `${watcher.lastStatus}${detail}`
  if (watcher.failureClass !== 'transient') {
    return `Auto-deploy could not install ${short} (${outcome}) — needs a person to look`
  }
  if (watcher.attempts >= maxAttempts) {
    return `Auto-deploy stopped retrying ${short} after ${watcher.attempts} failed attempts (${outcome}) — needs a person to look`
  }
  if (watcher.nextRetryAt === null) {
    return `Auto-deploy could not install ${short} (${outcome}) — no retry is scheduled; needs a person to look`
  }
  const retryAt = Date.parse(watcher.nextRetryAt)
  if (!Number.isFinite(retryAt)) {
    return `Auto-deploy could not install ${short} (${outcome}) — no retry is scheduled; needs a person to look`
  }
  const wait = Math.max(0, retryAt - nowMs)
  return `Auto-deploy could not install ${short}, attempt ${watcher.attempts}/${maxAttempts} (${outcome}) — retrying in ${durationLabel(wait)}`
}

function parseQueue(value: unknown): CiDeliveryQueue | null {
  if (value === null) return null
  const source = record(value)
  if (!source || !Array.isArray(source.requests)) throw new TypeError('deploy-status queue must contain requests')
  return {
    depth: nonNegativeNumber(source.depth, 'queue.depth')!,
    oldestAgeMs: nonNegativeNumber(source.oldestAgeMs, 'queue.oldestAgeMs', true),
    requests: source.requests.map((request, index) => {
      const sourceRequest = record(request)
      if (!sourceRequest) throw new TypeError(`deploy-status queue.requests[${index}] must be an object`)
      return {
        id: text(sourceRequest.id, `queue.requests[${index}].id`)!,
        observedAt: timestamp(sourceRequest.observedAt, `queue.requests[${index}].observedAt`)!,
      }
    }),
  }
}

function parseEvent(value: unknown): CiDeliveryEvent | null {
  if (value === null) return null
  const source = record(value)
  if (!source) throw new TypeError('deploy-status latestEvent must be an object or null')
  return {
    at: timestamp(source.at, 'latestEvent.at')!,
    type: text(source.type, 'latestEvent.type')!,
    detail: text(source.detail, 'latestEvent.detail', true),
  }
}

export function parseCiDeliveryData(value: unknown, freshness: CiDeliveryFreshness = 'unknown'): CiDeliveryData {
  if (value === undefined || value === null) return unknown('deploy-status data unavailable')

  try {
    const source = record(value)
    if (!source) throw new TypeError('deploy-status data must be an object')

    const status = source.state
    if (status !== 'idle' && status !== 'queued' && status !== 'running' && status !== 'stalled' && status !== 'unknown') {
      throw new TypeError('deploy-status state is invalid')
    }
    if (typeof source.complete !== 'boolean') throw new TypeError('deploy-status complete must be a boolean')

    const operation = text(source.operation, 'operation', true)
    const holder = text(source.holder, 'holder', true)
    const latestProgressAt = timestamp(source.latestProgressAt, 'latestProgressAt', true)
    const data: CiDeliveryData = {
      status,
      completeness: source.complete ? 'complete' : 'incomplete',
      freshness,
      observedAt: timestamp(source.observedAt, 'observedAt')!,
      queue: parseQueue(source.queue),
      current: operation === null && holder === null ? null : { operation, holder },
      latestEvent: parseEvent(source.latestEvent),
      latestProgressAt,
      reason: text(source.reason, 'reason', true),
      identity: parseIdentity(source),
      watcher: parseWatcher(source.watcher),
    }

    if (data.status === 'stalled' && data.latestProgressAt === null) {
      throw new TypeError('stalled deploy-status requires a latest progress timestamp')
    }
    return data
  } catch (error) {
    return unknown(error instanceof Error ? error.message : 'deploy-status data is malformed')
  }
}
