/**
 * Lifecycle inference.
 *
 * `inferColumnV0` places the coarse three columns from a single issues-list
 * read (state/labels/milestone). `inferLifecycle` (#5) adds the issue timeline:
 * first commit ref → Steeping, first PR ref → In review, close → Done — and
 * derives the estimate-vs-actual working time that feeds calibration (D3).
 */

import type { GiteaIssue } from '../gitea/types.js'

export type LifecycleColumn = 'diagnosis' | 'triage' | 'steeping' | 'review' | 'done'

export const LIFECYCLE_COLUMNS: readonly LifecycleColumn[] = [
  'diagnosis',
  'triage',
  'steeping',
  'review',
  'done',
]

/**
 * Closed → done. Open with any human intent applied (a label or a milestone)
 * → triage. Open and bare → diagnosis. Never returns steeping/review — that's
 * `inferLifecycle`, which reads the event stream.
 */
export function inferColumnV0(issue: Pick<GiteaIssue, 'state' | 'labels' | 'milestone'>): LifecycleColumn {
  if (issue.state === 'closed') return 'done'
  const hasIntent = issue.labels.length > 0 || issue.milestone !== null
  return hasIntent ? 'triage' : 'diagnosis'
}

/** A timeline signal, normalized from gitea's raw event stream. */
export type LifecycleEventType = 'triage' | 'commit' | 'pull' | 'close' | 'reopen'

export interface LifecycleEvent {
  type: LifecycleEventType
  /** ISO timestamp. */
  at: string
}

/** Timestamps for the stages an issue has reached (absent = not yet reached). */
export interface LifecycleStages {
  opened: string
  /** First label / milestone / assignment. */
  triaged?: string
  /** First commit referencing the issue — work began. */
  steeping?: string
  /** First PR referencing the issue — in review. */
  review?: string
  /** Closed. */
  done?: string
}

export interface LifecycleInference {
  column: LifecycleColumn
  stages: LifecycleStages
  /**
   * Working days from work-start (steeping → triaged → opened, first available)
   * to done. Only for closed issues — this is the "actual" calibration learns
   * from. null while open.
   */
  actualWorkingDays: number | null
  /** Working days the issue has been steeping (first commit → asOf); null unless currently steeping. */
  steepingDays: number | null
}

const DAY_MS = 86_400_000

/**
 * Whole working days (Mon–Fri) in the half-open interval [start, end). Same day
 * or reversed → 0. Day-granular by design — estimates are in days.
 */
export function workingDaysBetween(start: Date, end: Date): number {
  const s = Date.UTC(start.getUTCFullYear(), start.getUTCMonth(), start.getUTCDate())
  const e = Date.UTC(end.getUTCFullYear(), end.getUTCMonth(), end.getUTCDate())
  if (e <= s) return 0
  let count = 0
  for (let t = s; t < e; t += DAY_MS) {
    const dow = new Date(t).getUTCDay()
    if (dow !== 0 && dow !== 6) count += 1
  }
  return count
}

/**
 * The five-column inference. Column reflects the furthest stage still in play:
 * closed → done; else an open PR ref → review; else a commit ref → steeping;
 * else any triage signal (or a current label/milestone) → triage; else
 * diagnosis. Stage timestamps are the earliest event of each kind.
 */
export function inferLifecycle(
  issue: Pick<GiteaIssue, 'state' | 'labels' | 'milestone' | 'createdAt' | 'closedAt'>,
  events: LifecycleEvent[],
  asOf: Date,
): LifecycleInference {
  const sorted = [...events].sort((a, b) => a.at.localeCompare(b.at))
  const firstOf = (type: LifecycleEventType) => sorted.find((e) => e.type === type)?.at

  const stages: LifecycleStages = { opened: issue.createdAt }
  const triagedAt = firstOf('triage')
  const steepingAt = firstOf('commit')
  const reviewAt = firstOf('pull')
  if (triagedAt) stages.triaged = triagedAt
  if (steepingAt) stages.steeping = steepingAt
  if (reviewAt) stages.review = reviewAt
  const doneAt = issue.state === 'closed' ? (issue.closedAt ?? firstOf('close')) : undefined
  if (doneAt) stages.done = doneAt

  let column: LifecycleColumn
  if (issue.state === 'closed') column = 'done'
  else if (reviewAt) column = 'review'
  else if (steepingAt) column = 'steeping'
  else if (triagedAt || issue.labels.length > 0 || issue.milestone !== null) column = 'triage'
  else column = 'diagnosis'

  let actualWorkingDays: number | null = null
  if (stages.done) {
    const start = stages.steeping ?? stages.triaged ?? stages.opened
    actualWorkingDays = workingDaysBetween(new Date(start), new Date(stages.done))
  }

  let steepingDays: number | null = null
  if (column === 'steeping' && stages.steeping) {
    steepingDays = workingDaysBetween(new Date(stages.steeping), asOf)
  }

  return { column, stages, actualWorkingDays, steepingDays }
}
