import React, { useMemo } from 'react'
import type { ReactNode } from 'react'
import type {
  AgentAnimStatus,
  ProgressEntry,
  RoleAggregatedStatus,
  RoleWorkItemActivitySection,
  RoleWorkItemRow,
  RoleWorkItemSummary,
  Session,
  WorkItemProgressEntry,
} from '../types/kanban'
import { IconWorkItem } from './SvgIcons'
import { getWorkItemAssignmentLabel, humanizeWorkItemRoleId } from '../lib/workItemIdentity'
import { getExecutionTurnId } from '../lib/workItemRuntimeIds'

interface WorkItemProgressCardProps {
  workItemLog: WorkItemProgressEntry[]
  /**
   * Per-role DelegationWorkItem rollup. When present, drives the panel
   * (1 row = 1 work item; review work items appear under reviewer role).
   * Falls back to ``childSessions`` derivation only when undefined / empty
   * (legacy non-company-mode runs).
   * Source: ``snapshot_builder._build_role_work_items_for_session``.
   */
  roleWorkItems?: Record<string, RoleWorkItemSummary>
  /** Display-only executor-role rollup. Company/org Execution Progress
   *  prefers this so worker chips remain visible while awaiting review. */
  executorRoleWorkItems?: Record<string, RoleWorkItemSummary>
  /** Legacy session-based source. Kept for non-company-mode and for the
   *  case where a primary session has no ``roleWorkItems`` payload yet. */
  childSessions?: Session[]
  /** Company/org mode uses work-item rollups as the only role source. */
  isCompanyRuntime?: boolean
  onWorkItemClick?: (executionTurnId: string) => void
}

type WorkItemStatus = 'active' | 'done' | 'failed' | 'waiting' | 'pending'

interface WorkItemInfo {
  projectionId: string
  title: string
  roleName?: string
  status: WorkItemStatus
  executionTurnId?: string
}

interface RoleTurnInfo {
  executionTurnId: string
  title: string
  status: WorkItemStatus
  statusLabel: string       // kanban column label (To do / In progress / In review / Done)
  columnId: string          // todo | in_progress | in_review | done
  updatedAt: number
  /** Set when the row was derived from a DelegationWorkItem rather than
   *  a runtime Session. Used as the React key and for inline activity
   *  expansion. */
  workItemId?: string
  /** Per-row activity entries (already filtered by work-item projection
   *  on the backend). Empty for session-derived rows. */
  progressLog?: ProgressEntry[]
  activitySections?: RoleWorkItemActivitySection[]
  /** True when this row sits under the reviewer because the work item is
   *  in an ``in_review`` phase. */
  isReviewTarget?: boolean
}

interface RoleSummaryInfo {
  roleKey: string
  executionTurnId: string   // default click target = latest turn
  title: string             // role display name
  status: WorkItemStatus    // aggregated across all turns
  statusLabel: string       // kanban label for aggregated status
  executionAgent?: string
  roleName?: string
  updatedAt: number         // most recent turn's updatedAt
  turns: RoleTurnInfo[]     // chronological ASC (oldest first, newest last)
  /** Live tracker state (only set for work-item-driven summaries). When
   *  ``reflecting`` / ``tool_active`` the chip shows the orange pulse;
   *  otherwise the chip's colour is governed by ``status`` alone. */
  runtimeStatus?: AgentAnimStatus
}

/** Kanban-column labels used both on the role's aggregate chip and on
 *  each per-turn row.  Matches the column headers shown on the kanban
 *  board so the vocabulary stays consistent across views. */
const COLUMN_LABELS: Record<string, string> = {
  todo: 'To do',
  in_progress: 'In progress',
  in_review: 'In review',
  done: 'Done',
}

/** Map the WorkItemStatus derived from a Session back to the kanban column
 *  it sits in.  Keeps the label logic in one place rather than diverging
 *  between the role chip and the per-turn row. */
function workItemStatusToColumnId(status: WorkItemStatus, fallbackColumnId?: string): string {
  if (fallbackColumnId && fallbackColumnId in COLUMN_LABELS) return fallbackColumnId
  switch (status) {
    case 'active':  return 'in_progress'
    case 'waiting': return 'in_review'
    case 'done':    return 'done'
    case 'failed':  return 'done'
    case 'pending': return 'todo'
    default:        return 'todo'
  }
}

function labelForColumnId(columnId: string): string {
  return COLUMN_LABELS[columnId] ?? COLUMN_LABELS.todo
}

/** Priority used to aggregate status across a role's turns so the main
 *  chip reflects "any turn still in flight" rather than whatever was
 *  updated last. */
const STATUS_AGGREGATE_PRIORITY: Record<WorkItemStatus, number> = {
  active: 0,
  waiting: 1,
  failed: 2,
  done: 3,
  pending: 4,
}

function aggregateStatus(statuses: WorkItemStatus[]): WorkItemStatus {
  if (statuses.length === 0) return 'pending'
  let best: WorkItemStatus = statuses[0]
  let bestRank = STATUS_AGGREGATE_PRIORITY[best] ?? 99
  for (const s of statuses) {
    const rank = STATUS_AGGREGATE_PRIORITY[s] ?? 99
    if (rank < bestRank) {
      best = s
      bestRank = rank
    }
  }
  return best
}

const EXECUTION_AGENT_LABELS: Record<string, string> = {
  native: 'Native',
  codex: 'Codex',
  claude_code: 'Claude Code',
  cursor: 'Cursor',
  opencode: 'OpenCode',
}

/** Check if a string looks like a UUID or long hex id */
function isUuidLike(s: string): boolean {
  return s.length > 12 && /^[0-9a-f-]+$/i.test(s.replace(/_/g, ''))
}

function trimString(value: unknown): string {
  return typeof value === 'string' ? value.trim() : ''
}

function firstNonEmpty(...values: unknown[]): string {
  for (const value of values) {
    const text = trimString(value)
    if (text) return text
  }
  return ''
}

function formatExecutionAgent(value?: string): string {
  const normalized = trimString(value)
  if (!normalized) return ''
  return EXECUTION_AGENT_LABELS[normalized] ?? humanizeWorkItemRoleId(normalized)
}

function renderProjectionIcon(status: WorkItemStatus): ReactNode {
  if (status === 'done') return <span className="wi-projection-icon">&#10003;</span>
  if (status === 'active') {
    // Always pulse when the role is "active". Active aggregates two cases
    // — (a) tracker is reflecting/tool_active right now, or (b) at least
    // one work item is in an in-progress phase. Both read as "this role
    // is working" to the user, so both should breathe. The earlier
    // refactor that gated pulse on runtime tracker only made finished-but-
    // running phases look static, which the user reported as a regression.
    return <span className="wi-projection-icon wi-projection-pulse">&#9679;</span>
  }
  if (status === 'failed') return <span className="wi-projection-icon">&#10007;</span>
  if (status === 'waiting') return <span className="wi-projection-icon">&#9679;</span>
  return null
}

const AGGREGATED_TO_WORK_ITEM_STATUS: Record<RoleAggregatedStatus, WorkItemStatus> = {
  active: 'active',
  waiting: 'waiting',
  pending: 'pending',
  done: 'done',
  failed: 'failed',
}

/** Backend ``kanban_column`` returns hyphenated ids (``in-progress`` /
 *  ``in-review``); this card's CSS classes use underscored forms. The
 *  conversion lives here so the rest of the file keeps speaking one
 *  vocabulary. */
function normalizeColumnId(columnId: string): string {
  if (!columnId) return 'todo'
  if (columnId === 'in-progress') return 'in_progress'
  if (columnId === 'in-review') return 'in_review'
  return columnId
}

/** Per-row status used for icon + chip colour. Mirrors the backend
 *  phase → column mapping (``opc/layer2_organization/phase.py``) but
 *  reduced to the 5 UI states the chip renders. */
function phaseAggregateForRow(phase: string): WorkItemStatus {
  switch (phase) {
    case 'running':
    case 'waiting_for_peer':
    case 'waiting_for_children':
    case 'paused':
    case 'needs_attention':
      return 'active'
    case 'awaiting_manager_review':
    case 'awaiting_human':
    case 'queued':
    case 'ready':
    case 'ready_for_rework':
    case 'waiting_dependencies':
      return 'waiting'
    case 'approved':
      return 'done'
    case 'failed':
    case 'cancelled':
      return 'failed'
    default:
      return 'pending'
  }
}

function statusFromSession(session: Session): WorkItemStatus {
  const status = trimString(session.status).toLowerCase()
  if (status === 'done' || status === 'delivered') return 'done'
  if (status === 'failed' || status === 'cancelled') return 'failed'
  if (
    status === 'awaiting_peer'
    || status === 'awaiting_manager_review'
    || status === 'awaiting_human'
    || status === 'awaiting_review'
    || status === 'blocked'
    || status === 'paused'
    || status === 'awaiting_owner'
  ) {
    return 'waiting'
  }
  if (status === 'running' || status === 'deliverable' || status === 'active' || status === 'ready') return 'active'
  return 'pending'
}

export function WorkItemProgressCard({
  workItemLog,
  roleWorkItems,
  executorRoleWorkItems,
  childSessions,
  isCompanyRuntime = false,
  onWorkItemClick,
}: WorkItemProgressCardProps) {
  // Build child session lookup for enriching display names
  const sessionByTaskId = useMemo(() => {
    const map = new Map<string, Session>()
    for (const s of childSessions ?? []) {
      map.set(s.taskId, s)
      const executionTurnId = getExecutionTurnId(s)
      if (executionTurnId) map.set(executionTurnId, s)
    }
    return map
  }, [childSessions])

  // Build ordered work-item list from log entries — used
  // only as a fallback when no child sessions exist yet, because the
  // work-item log emits one projection id per runtime turn which means a single
  // role can appear multiple times (execute + each review + attention).
  // For kanban-push runs we'd rather derive the pipeline from
  // roleSummaries below, which already groups by workItemRoleId.
  const workItemLogWorkItems = useMemo(() => {
    const map = new Map<string, WorkItemInfo>()
    const order: string[] = []

    for (const entry of workItemLog) {
      const entryExecutionTurnId = getExecutionTurnId(entry)
      const projectionId = entry.workItemProjectionId ?? entryExecutionTurnId ?? ''
      if (!projectionId) continue

      if (!map.has(projectionId)) {
        order.push(projectionId)
        // Determine display title — prefer roleName, avoid UUID fallback
        const rawTitle = entry.workItemProjectionTitle ?? projectionId
        const title = entry.roleName || (!isUuidLike(rawTitle) ? rawTitle : 'Agent')
        map.set(projectionId, {
          projectionId,
          title,
          roleName: entry.roleName,
          status: 'pending',
          executionTurnId: entryExecutionTurnId,
        })
      }

      const info = map.get(projectionId)!
      // Update with latest role/title info
      if (entry.roleName) {
        info.roleName = entry.roleName
        info.title = entry.roleName
      } else if (entry.workItemProjectionTitle && !isUuidLike(entry.workItemProjectionTitle)) {
        info.title = entry.workItemProjectionTitle
      }
      if (entryExecutionTurnId) info.executionTurnId = entryExecutionTurnId

      // Update status based on event type
      switch (entry.type) {
        case 'work_item_started': info.status = 'active'; break
        case 'gate_approved': info.status = 'done'; break
        case 'gate_rejected': info.status = 'active'; break
        case 'awaiting_manager_review':
        case 'awaiting_human':
        case 'awaiting_review':
        case 'awaiting_peer': info.status = 'waiting'; break
        case 'work_item_failed':
        case 'deadlock': info.status = 'failed'; break
      }
    }

    // Enrich from child sessions
    for (const info of map.values()) {
      if (!info.executionTurnId) continue
      const session = sessionByTaskId.get(info.executionTurnId)
      if (!session) continue
      const label = getWorkItemAssignmentLabel(session)
      if (label) info.title = label
      if (session.workItemRoleName) info.roleName = session.workItemRoleName
      if (session.status === 'done') info.status = 'done'
      else if (session.status === 'failed') info.status = 'failed'
    }

    return order.map(id => map.get(id)!)
  }, [workItemLog, sessionByTaskId])

  const taskOrder = useMemo(() => {
    const map = new Map<string, number>()
    for (const entry of workItemLog) {
      const taskId = getExecutionTurnId(entry)
      if (taskId && !map.has(taskId)) map.set(taskId, map.size)
    }
    return map
  }, [workItemLog])

  // Primary path: drive rows directly from the per-role DelegationWorkItem
  // rollup the backend ships in ``session.role_work_items``. This is the
  // fix for "1 row should = 1 work item" — the legacy session-driven
  // derivation below treats every runtime Task as its own row, which
  // double-counts rework turns and silently drops queued / review-target
  // work items. See ``plan/bug-breezy-dragonfly.md`` for the full root-cause
  // breakdown.
  const displayRoleWorkItems = executorRoleWorkItems ?? roleWorkItems
  const roleSummariesFromWorkItems = useMemo<RoleSummaryInfo[]>(() => {
    if (!displayRoleWorkItems) return []
    const summaries: RoleSummaryInfo[] = []
    for (const summary of Object.values(displayRoleWorkItems)) {
      if (!summary || !Array.isArray(summary.workItems) || summary.workItems.length === 0) continue
      // Backend already sorts ASC by createdAt; defensive copy + re-sort
      // here means a prop-mutation upstream can't reorder the rows.
      const ordered = [...summary.workItems].sort((a, b) => a.createdAt - b.createdAt)
      const turns: RoleTurnInfo[] = ordered.map((row: RoleWorkItemRow) => {
        const columnId = normalizeColumnId(row.kanbanColumn)
        const phaseToStatus = phaseAggregateForRow(row.phase)
        return {
          executionTurnId: row.executionTurnId ?? '',
          title: trimString(row.title) || trimString(row.executorRoleName) || trimString(row.executorRoleId) || 'Work item',
          status: phaseToStatus,
          statusLabel: labelForColumnId(columnId),
          columnId,
          updatedAt: row.updatedAt,
          workItemId: row.workItemId,
          progressLog: row.progressLog,
          activitySections: row.activitySections ?? [],
          isReviewTarget: row.isReviewTarget,
        }
      })
      const aggregatedStatus = AGGREGATED_TO_WORK_ITEM_STATUS[summary.aggregatedStatus] ?? 'pending'
      const aggregatedColumnId = workItemStatusToColumnId(aggregatedStatus)
      const latest = turns[turns.length - 1]
      // Chip click target: prefer the latest turn's runtime task. When the
      // last work item has no execution turn yet (queued / never dispatched),
      // walk back to find the most recent dispatched turn.
      const fallbackExecutionTurnId = [...turns].reverse().find(t => !!t.executionTurnId)?.executionTurnId ?? ''
      summaries.push({
        roleKey: summary.roleKey,
        executionTurnId: latest.executionTurnId || fallbackExecutionTurnId,
        title: summary.roleName || summary.roleId,
        status: aggregatedStatus,
        statusLabel: labelForColumnId(aggregatedColumnId),
        roleName: summary.roleName,
        updatedAt: turns.reduce((max, t) => Math.max(max, t.updatedAt), 0),
        turns,
        runtimeStatus: summary.runtimeStatus,
      })
    }
    // Stable order: roles first appearing in time go left.
    summaries.sort((a, b) => {
      const aFirst = a.turns[0]?.updatedAt ?? Number.POSITIVE_INFINITY
      const bFirst = b.turns[0]?.updatedAt ?? Number.POSITIVE_INFINITY
      return aFirst - bFirst
    })
    return summaries
  }, [displayRoleWorkItems])

  const roleSummariesFromSessions = useMemo<RoleSummaryInfo[]>(() => {
    const sessions = [...(childSessions ?? [])]
    if (sessions.length === 0) return []

    // Group by workItemRoleId (fall back to the assignee when missing
    // so legacy non-company-mode sessions still get a stable key).
    const groups = new Map<string, Session[]>()
    const groupOrder: string[] = []
    for (const session of sessions) {
      const roleKey = firstNonEmpty(
        session.workItemRoleId,
        session.assigneeIds[0],
        session.taskId,         // last-resort singleton
      )
      if (!groups.has(roleKey)) {
        groups.set(roleKey, [])
        groupOrder.push(roleKey)
      }
      groups.get(roleKey)!.push(session)
    }

    // Sort role groups by the earliest work-item-log/updatedAt position
    // of any turn in the group so the bar reads left-to-right in the
    // order roles first appeared.
    groupOrder.sort((leftKey, rightKey) => {
      const leftSessions = groups.get(leftKey) ?? []
      const rightSessions = groups.get(rightKey) ?? []
      const leftOrder = Math.min(
        ...leftSessions.map(s => taskOrder.get(getExecutionTurnId(s) || s.taskId) ?? Number.POSITIVE_INFINITY),
      )
      const rightOrder = Math.min(
        ...rightSessions.map(s => taskOrder.get(getExecutionTurnId(s) || s.taskId) ?? Number.POSITIVE_INFINITY),
      )
      if (leftOrder !== rightOrder) return leftOrder - rightOrder
      const leftUpdated = Math.min(...leftSessions.map(s => s.updatedAt))
      const rightUpdated = Math.min(...rightSessions.map(s => s.updatedAt))
      return leftUpdated - rightUpdated
    })

    const summaries: RoleSummaryInfo[] = []
    for (const roleKey of groupOrder) {
      const roleSessions = groups.get(roleKey) ?? []
      if (roleSessions.length === 0) continue

      // Build one turn entry per session, sorted chronologically (ASC).
      // Per design: "a 最前面, c 最后面" — first-happened goes first.
      // Each turn shows only its kanban column (To do / In progress /
      // In review / Done).  No synthetic "kind" classification.
      const turns: RoleTurnInfo[] = roleSessions
        .slice()
        .sort((a, b) => a.updatedAt - b.updatedAt)
        .map((session) => {
          const status = statusFromSession(session)
          const executionTurnId = getExecutionTurnId(session)
          const columnId = workItemStatusToColumnId(
            status,
            String((session as Session & { columnId?: string }).columnId ?? '').trim() || undefined,
          )
          return {
            executionTurnId: executionTurnId || session.taskId,
            title: firstNonEmpty(
              getWorkItemAssignmentLabel(session),
              session.title,
              session.workItemRoleName,
              session.workItemRoleId,
              'Turn',
            ),
            status,
            statusLabel: labelForColumnId(columnId),
            columnId,
            updatedAt: session.updatedAt,
          }
        })

      // The newest turn is the one the user probably wants to open when
      // they click the main chip — "what is this role doing right now"
      // beats "what did this role do first".
      const latest = turns[turns.length - 1]

      const aggregatedStatus = aggregateStatus(turns.map(t => t.status))
      const aggregatedColumnId = workItemStatusToColumnId(aggregatedStatus)

      // Role name beats task title on the main chip so the chip always
      // reads as "this role" regardless of which of its turns is loaded
      // as latest.
      const representative = roleSessions[0]
      const title = firstNonEmpty(
        representative.workItemRoleName,
        representative.workItemRoleId,
        representative.assigneeIds[0],
        getWorkItemAssignmentLabel(representative),
        representative.title,
        'Agent',
      )

      summaries.push({
        roleKey,
        executionTurnId: latest.executionTurnId,
        title,
        status: aggregatedStatus,
        statusLabel: labelForColumnId(aggregatedColumnId),
        executionAgent: formatExecutionAgent(
          representative.selectedExecutionAgent ?? representative.preferredAgent,
        ),
        roleName: representative.workItemRoleName,
        updatedAt: Math.max(...roleSessions.map(s => s.updatedAt)),
        turns,
      })
    }

    return summaries
  }, [childSessions, taskOrder])

  // Pick the work-item-driven summaries when present. In company/org
  // mode, runtime sessions are only audit targets; they must not synthesize
  // role rows because that recreates the role/session mix-up seen in new37.
  const roleSummaries = useMemo<RoleSummaryInfo[]>(() => (
    roleSummariesFromWorkItems.length > 0
      ? roleSummariesFromWorkItems
      : (isCompanyRuntime ? [] : roleSummariesFromSessions)
  ), [isCompanyRuntime, roleSummariesFromWorkItems, roleSummariesFromSessions])

  // Top-level pipeline: one chip per role, derived from roleSummaries
  // when available (kanban-push runs). Fall back to work-item-log work items
  // when child sessions haven't been serialized yet.
  const workItems = useMemo<WorkItemInfo[]>(() => {
    if (roleSummaries.length > 0) {
      return roleSummaries.map(role => ({
        projectionId: role.roleKey,
        title: role.title,
        roleName: role.roleName,
        status: role.status,
        executionTurnId: role.executionTurnId,
      }))
    }
    return isCompanyRuntime ? [] : workItemLogWorkItems
  }, [isCompanyRuntime, roleSummaries, workItemLogWorkItems])

  if (isCompanyRuntime && roleSummaries.length === 0) return null
  if (workItemLog.length === 0 && workItems.length === 0 && roleSummaries.length === 0) return null

  return (
    <div className="wi-progress-card">
      <div className="wi-progress-header">
        <IconWorkItem />
        <span>Execution Progress</span>
      </div>

      {workItems.length > 0 && (
        <div className="wi-progress-pipeline">
          {workItems.map((workItem, i) => (
            <div key={workItem.projectionId} className="wi-projection-group">
              <button
                type="button"
                className={`wi-projection-chip wi-projection-${workItem.status}`}
                onClick={() => onWorkItemClick?.(workItem.executionTurnId || '')}
                title={`Open Runtime Session${workItem.roleName ? `: ${workItem.title} (${workItem.roleName})` : `: ${workItem.title}`}`}
              >
                <span className="wi-projection-label">{workItem.title}</span>
                {renderProjectionIcon(workItem.status)}
              </button>
              {i < workItems.length - 1 && <span className="wi-projection-connector">&rarr;</span>}
            </div>
          ))}
        </div>
      )}

    </div>
  )
}
