import { create } from 'zustand'
import type {
  ClaudeJobEvent,
  JobState,
  RealtimeEvent,
  RealtimeStatus,
  ResyncReason,
  SoloColumnStatus,
  SoloTask,
  SoloTaskStatus,
  SoloUnassignedStory,
  SoloWorkspaceProduct,
  SoloWorkspaceSnapshot,
  SoloWorkspaceSprint,
  SoloVerifyRequired,
} from './types'

interface ContextSlice {
  activeProduct: SoloWorkspaceProduct | null
  activeSprint: SoloWorkspaceSprint | null
  activeUserId: string | null
}

interface EntitiesSlice {
  tasksById: Record<string, SoloTask>
  unassignedStoriesById: Record<string, SoloUnassignedStory>
  jobsByTaskId: Record<string, JobState>
}

interface RelationsSlice {
  taskIdsByColumn: Record<SoloColumnStatus, string[]>
  unassignedStoryIds: string[]
}

interface LoadingSlice {
  loadedProductId: string | null
  loadedSprintId: string | null
  loadingSprintId: string | null
  activeRequestId: string | null
}

interface SyncSlice {
  realtimeStatus: RealtimeStatus
  showConnectingIndicator: boolean
  lastEventAt: number | null
  lastResyncAt: number | null
  resyncReason: ResyncReason | null
}

interface State {
  context: ContextSlice
  entities: EntitiesSlice
  relations: RelationsSlice
  loading: LoadingSlice
  sync: SyncSlice
  pendingOps: Set<string>
  tasks: Record<string, SoloTask>
  unassignedStoriesById: Record<string, SoloUnassignedStory>
  claudeJobsByTaskId: Record<string, JobState>
  realtimeStatus: RealtimeStatus
  showConnectingIndicator: boolean
  connectedWorkers: number
  workerQuotaPct: number | null
  workerQuotaCheckAt: string | null
}

interface Actions {
  hydrateSnapshot(snapshot: SoloWorkspaceSnapshot): void
  initTasks(tasks: SoloTask[]): void
  hydrateUnassignedStories(stories: SoloUnassignedStory[]): void
  removeUnassignedStory(storyId: string): void

  optimisticMove(taskId: string, toStatus: SoloTaskStatus): SoloTaskStatus | null
  rollback(taskId: string, prevStatus: SoloTaskStatus): void
  updatePlan(taskId: string, plan: string | null): void
  updateVerifyOnly(taskId: string, value: boolean): void
  updateVerifyRequired(taskId: string, value: SoloVerifyRequired): void

  markPending(taskId: string): void
  clearPending(taskId: string): void

  setRealtimeStatus(status: RealtimeStatus, showConnectingIndicator: boolean): void

  initJobs(jobs: JobState[]): void
  handleJobEvent(event: ClaudeJobEvent): void

  setWorkers(count: number): void
  incrementWorkers(): void
  decrementWorkers(): void
  setWorkerQuota(pct: number, checkAt: string): void

  handleRealtimeEvent(event: RealtimeEvent): void
  ensureWorkspaceLoaded(productId: string, sprintId?: string, requestId?: string): Promise<void>
  resyncActiveScopes(reason: ResyncReason): Promise<void>
}

export type SoloWorkspaceStore = State & Actions

const EMPTY_COLUMNS: Record<SoloColumnStatus, string[]> = {
  TO_DO: [],
  IN_PROGRESS: [],
  DONE: [],
}

const initialState: State = {
  context: {
    activeProduct: null,
    activeSprint: null,
    activeUserId: null,
  },
  entities: {
    tasksById: {},
    unassignedStoriesById: {},
    jobsByTaskId: {},
  },
  relations: {
    taskIdsByColumn: EMPTY_COLUMNS,
    unassignedStoryIds: [],
  },
  loading: {
    loadedProductId: null,
    loadedSprintId: null,
    loadingSprintId: null,
    activeRequestId: null,
  },
  sync: {
    realtimeStatus: 'connecting',
    showConnectingIndicator: false,
    lastEventAt: null,
    lastResyncAt: null,
    resyncReason: null,
  },
  pendingOps: new Set<string>(),
  tasks: {},
  unassignedStoriesById: {},
  claudeJobsByTaskId: {},
  realtimeStatus: 'connecting',
  showConnectingIndicator: false,
  connectedWorkers: 0,
  workerQuotaPct: null,
  workerQuotaCheckAt: null,
}

function getColumnStatus(status: SoloTaskStatus): SoloColumnStatus {
  if (status === 'REVIEW') return 'IN_PROGRESS'
  return status
}

function compareTask(a: SoloTask, b: SoloTask): number {
  if (a.sort_order !== b.sort_order) return a.sort_order - b.sort_order
  if (a.priority !== b.priority) return a.priority - b.priority
  const aCode = a.task_code ?? ''
  const bCode = b.task_code ?? ''
  const codeCompare = aCode.localeCompare(bCode, 'nl', { numeric: true })
  if (codeCompare !== 0) return codeCompare
  return a.id.localeCompare(b.id)
}

function compareUnassignedStory(a: SoloUnassignedStory, b: SoloUnassignedStory): number {
  const aCode = a.code ?? ''
  const bCode = b.code ?? ''
  const codeCompare = aCode.localeCompare(bCode, 'nl', { numeric: true })
  if (codeCompare !== 0) return codeCompare
  return a.title.localeCompare(b.title, 'nl', { numeric: true })
}

function buildTaskRelations(tasksById: Record<string, SoloTask>): Record<SoloColumnStatus, string[]> {
  const next: Record<SoloColumnStatus, string[]> = {
    TO_DO: [],
    IN_PROGRESS: [],
    DONE: [],
  }
  const tasks = Object.values(tasksById).sort(compareTask)
  for (const task of tasks) {
    next[getColumnStatus(task.status)].push(task.id)
  }
  return next
}

function buildUnassignedRelations(storiesById: Record<string, SoloUnassignedStory>): string[] {
  return Object.values(storiesById)
    .sort(compareUnassignedStory)
    .map((story) => story.id)
}

function normalizeTask(input: SoloTask): SoloTask {
  return {
    ...input,
    status: normalizeTaskStatus(input.status),
  }
}

function normalizeTaskStatus(status: string): SoloTaskStatus {
  if (status === 'IN_PROGRESS' || status === 'REVIEW' || status === 'DONE') return status
  return 'TO_DO'
}

function mapTasks(tasks: SoloTask[]): Record<string, SoloTask> {
  return Object.fromEntries(tasks.map((task) => [task.id, normalizeTask(task)]))
}

function mapUnassignedStories(stories: SoloUnassignedStory[]): Record<string, SoloUnassignedStory> {
  return Object.fromEntries(stories.map((story) => [story.id, story]))
}

function newRequestId(): string {
  if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
    return crypto.randomUUID()
  }
  return `${Date.now()}-${Math.random().toString(36).slice(2)}`
}

async function fetchJson<T>(url: string, init?: RequestInit): Promise<T> {
  const response = await fetch(url, { cache: 'no-store', ...init })
  if (!response.ok) {
    throw new Error(`Fetch ${url} failed with ${response.status}`)
  }
  return (await response.json()) as T
}

function taskPatchFromEvent(event: RealtimeEvent): Partial<SoloTask> {
  const status = event.status ?? event.task_status
  return {
    ...(status && { status: normalizeTaskStatus(status) }),
    ...((event.sort_order ?? event.task_sort_order) !== undefined && {
      sort_order: event.sort_order ?? event.task_sort_order,
    }),
    ...((event.title ?? event.task_title) !== undefined && {
      title: event.title ?? event.task_title,
    }),
    ...(event.description !== undefined && { description: event.description }),
    ...(event.priority !== undefined && { priority: event.priority }),
    ...(event.story_id !== undefined && { story_id: event.story_id }),
  }
}

function storyTitleFromEvent(event: RealtimeEvent): string | undefined {
  return event.title ?? event.story_title
}

function storyCodeFromEvent(event: RealtimeEvent): string | null | undefined {
  return event.code ?? event.story_code
}

export const useSoloWorkspaceStore = create<SoloWorkspaceStore>((set, get) => ({
  ...initialState,

  hydrateSnapshot(snapshot) {
    const tasksById = mapTasks(snapshot.tasks)
    const unassignedStoriesById = mapUnassignedStories(snapshot.unassignedStories)
    set((s) => ({
      context: {
        activeProduct: snapshot.product,
        activeSprint: snapshot.sprint,
        activeUserId: snapshot.activeUserId,
      },
      entities: {
        ...s.entities,
        tasksById,
        unassignedStoriesById,
      },
      relations: {
        taskIdsByColumn: buildTaskRelations(tasksById),
        unassignedStoryIds: buildUnassignedRelations(unassignedStoriesById),
      },
      loading: {
        ...s.loading,
        loadedProductId: snapshot.product.id,
        loadedSprintId: snapshot.sprint.id,
        loadingSprintId: null,
      },
      tasks: tasksById,
      unassignedStoriesById,
    }))
  },

  initTasks(tasks) {
    const tasksById = mapTasks(tasks)
    set((s) => ({
      entities: { ...s.entities, tasksById },
      relations: {
        ...s.relations,
        taskIdsByColumn: buildTaskRelations(tasksById),
      },
      tasks: tasksById,
    }))
  },

  hydrateUnassignedStories(stories) {
    const unassignedStoriesById = mapUnassignedStories(stories)
    set((s) => ({
      entities: { ...s.entities, unassignedStoriesById },
      relations: {
        ...s.relations,
        unassignedStoryIds: buildUnassignedRelations(unassignedStoriesById),
      },
      unassignedStoriesById,
    }))
  },

  removeUnassignedStory(storyId) {
    set((s) => {
      if (!s.entities.unassignedStoriesById[storyId]) return s
      const unassignedStoriesById = { ...s.entities.unassignedStoriesById }
      delete unassignedStoriesById[storyId]
      return {
        entities: { ...s.entities, unassignedStoriesById },
        relations: {
          ...s.relations,
          unassignedStoryIds: buildUnassignedRelations(unassignedStoriesById),
        },
        unassignedStoriesById,
      }
    })
  },

  optimisticMove(taskId, toStatus) {
    const prev = get().tasks[taskId]?.status ?? null
    if (!prev) return null
    const task = { ...get().tasks[taskId], status: toStatus }
    const tasksById = { ...get().tasks, [taskId]: task }
    set((s) => ({
      entities: { ...s.entities, tasksById },
      relations: { ...s.relations, taskIdsByColumn: buildTaskRelations(tasksById) },
      tasks: tasksById,
    }))
    return prev
  },

  rollback(taskId, prevStatus) {
    const existing = get().tasks[taskId]
    if (!existing) return
    const tasksById = { ...get().tasks, [taskId]: { ...existing, status: prevStatus } }
    set((s) => ({
      entities: { ...s.entities, tasksById },
      relations: { ...s.relations, taskIdsByColumn: buildTaskRelations(tasksById) },
      tasks: tasksById,
    }))
  },

  updatePlan(taskId, plan) {
    const existing = get().tasks[taskId]
    if (!existing) return
    const tasksById = { ...get().tasks, [taskId]: { ...existing, implementation_plan: plan } }
    set((s) => ({ entities: { ...s.entities, tasksById }, tasks: tasksById }))
  },

  updateVerifyOnly(taskId, value) {
    const existing = get().tasks[taskId]
    if (!existing) return
    const tasksById = { ...get().tasks, [taskId]: { ...existing, verify_only: value } }
    set((s) => ({ entities: { ...s.entities, tasksById }, tasks: tasksById }))
  },

  updateVerifyRequired(taskId, value) {
    const existing = get().tasks[taskId]
    if (!existing) return
    const tasksById = { ...get().tasks, [taskId]: { ...existing, verify_required: value } }
    set((s) => ({ entities: { ...s.entities, tasksById }, tasks: tasksById }))
  },

  markPending(taskId) {
    set((s) => {
      if (s.pendingOps.has(taskId)) return s
      const pendingOps = new Set(s.pendingOps)
      pendingOps.add(taskId)
      return { pendingOps }
    })
  },

  clearPending(taskId) {
    set((s) => {
      if (!s.pendingOps.has(taskId)) return s
      const pendingOps = new Set(s.pendingOps)
      pendingOps.delete(taskId)
      return { pendingOps }
    })
  },

  setRealtimeStatus(status, showConnectingIndicator) {
    set((s) => {
      if (s.realtimeStatus === status && s.showConnectingIndicator === showConnectingIndicator) {
        return s
      }
      return {
        sync: { ...s.sync, realtimeStatus: status, showConnectingIndicator },
        realtimeStatus: status,
        showConnectingIndicator,
      }
    })
  },

  initJobs(jobs) {
    const jobsByTaskId = Object.fromEntries(jobs.map((job) => [job.task_id, job]))
    set((s) => ({
      entities: { ...s.entities, jobsByTaskId },
      claudeJobsByTaskId: jobsByTaskId,
    }))
  },

  handleJobEvent(event) {
    const { job_id, task_id } = event
    if (event.type === 'claude_job_enqueued') {
      set((s) => {
        const jobsByTaskId = {
          ...s.claudeJobsByTaskId,
          [task_id]: { job_id, task_id, status: 'queued' as const },
        }
        return {
          entities: { ...s.entities, jobsByTaskId },
          claudeJobsByTaskId: jobsByTaskId,
        }
      })
      return
    }

    const { status, branch, pushed_at, pr_url, verify_result, summary, error } = event
    if (status === 'cancelled') {
      set((s) => {
        const jobsByTaskId = { ...s.claudeJobsByTaskId }
        delete jobsByTaskId[task_id]
        return {
          entities: { ...s.entities, jobsByTaskId },
          claudeJobsByTaskId: jobsByTaskId,
        }
      })
      return
    }

    set((s) => {
      const jobsByTaskId = {
        ...s.claudeJobsByTaskId,
        [task_id]: {
          job_id,
          task_id,
          status,
          branch,
          pushed_at,
          pr_url,
          verify_result,
          summary,
          error,
        },
      }
      return {
        entities: { ...s.entities, jobsByTaskId },
        claudeJobsByTaskId: jobsByTaskId,
      }
    })
  },

  setWorkers(count) {
    set({ connectedWorkers: Math.max(0, count) })
  },

  incrementWorkers() {
    set((s) => ({ connectedWorkers: s.connectedWorkers + 1 }))
  },

  decrementWorkers() {
    set((s) => ({
      connectedWorkers: Math.max(0, s.connectedWorkers - 1),
      workerQuotaPct: s.connectedWorkers - 1 <= 0 ? null : s.workerQuotaPct,
      workerQuotaCheckAt: s.connectedWorkers - 1 <= 0 ? null : s.workerQuotaCheckAt,
    }))
  },

  setWorkerQuota(pct, checkAt) {
    set({ workerQuotaPct: pct, workerQuotaCheckAt: checkAt })
  },

  handleRealtimeEvent(event) {
    set((s) => ({ sync: { ...s.sync, lastEventAt: Date.now() } }))

    const ctx = get().context
    if (ctx.activeProduct?.id && event.product_id !== ctx.activeProduct.id) return

    if (event.entity === 'task') {
      if (event.op === 'D') {
        const existing = get().tasks[event.id]
        if (!existing) return
        const tasksById = { ...get().tasks }
        delete tasksById[event.id]
        set((s) => ({
          entities: { ...s.entities, tasksById },
          relations: { ...s.relations, taskIdsByColumn: buildTaskRelations(tasksById) },
          tasks: tasksById,
        }))
        return
      }

      const existing = get().tasks[event.id]
      if (!existing) {
        if (
          event.assignee_id === ctx.activeUserId &&
          event.sprint_id === ctx.activeSprint?.id
        ) {
          void get().resyncActiveScopes('unknown-event')
        }
        return
      }

      if (
        event.assignee_id !== null &&
        ctx.activeUserId &&
        event.assignee_id !== ctx.activeUserId
      ) {
        const tasksById = { ...get().tasks }
        delete tasksById[event.id]
        set((s) => ({
          entities: { ...s.entities, tasksById },
          relations: { ...s.relations, taskIdsByColumn: buildTaskRelations(tasksById) },
          tasks: tasksById,
        }))
        return
      }

      if (get().pendingOps.has(event.id)) return

      const patch = taskPatchFromEvent(event)
      if (Object.keys(patch).length === 0) return
      const tasksById = {
        ...get().tasks,
        [event.id]: { ...existing, ...patch },
      }
      set((s) => ({
        entities: { ...s.entities, tasksById },
        relations: { ...s.relations, taskIdsByColumn: buildTaskRelations(tasksById) },
        tasks: tasksById,
      }))
      return
    }

    if (event.op === 'D') {
      const tasksById = Object.fromEntries(
        Object.entries(get().tasks).filter(([, task]) => task.story_id !== event.id),
      )
      const unassignedStoriesById = { ...get().entities.unassignedStoriesById }
      delete unassignedStoriesById[event.id]
      set((s) => ({
        entities: { ...s.entities, tasksById, unassignedStoriesById },
        relations: {
          taskIdsByColumn: buildTaskRelations(tasksById),
          unassignedStoryIds: buildUnassignedRelations(unassignedStoriesById),
        },
        tasks: tasksById,
        unassignedStoriesById,
      }))
      return
    }

    const affectedIds = Object.entries(get().tasks)
      .filter(([, task]) => task.story_id === event.id)
      .map(([taskId]) => taskId)
    const newTitle = storyTitleFromEvent(event)
    const newCode = storyCodeFromEvent(event)

    if (affectedIds.length > 0 && (newTitle !== undefined || newCode !== undefined)) {
      const tasksById = { ...get().tasks }
      for (const taskId of affectedIds) {
        const task = tasksById[taskId]
        tasksById[taskId] = {
          ...task,
          ...(newTitle !== undefined && { story_title: newTitle }),
          ...(newCode !== undefined && { story_code: newCode }),
        }
      }
      set((s) => ({
        entities: { ...s.entities, tasksById },
        relations: { ...s.relations, taskIdsByColumn: buildTaskRelations(tasksById) },
        tasks: tasksById,
      }))
    }

    if (
      event.sprint_id === ctx.activeSprint?.id &&
      (event.assignee_id === null || event.assignee_id === ctx.activeUserId)
    ) {
      void get().resyncActiveScopes('unknown-event')
    }
  },

  async ensureWorkspaceLoaded(productId, sprintId, requestId) {
    const activeRequestId = requestId ?? newRequestId()
    set((s) => ({
      loading: {
        ...s.loading,
        loadingSprintId: sprintId ?? s.context.activeSprint?.id ?? null,
        activeRequestId,
      },
    }))
    try {
      const params = sprintId ? `?sprint_id=${encodeURIComponent(sprintId)}` : ''
      const snapshot = await fetchJson<SoloWorkspaceSnapshot | null>(
        `/api/products/${encodeURIComponent(productId)}/solo-workspace${params}`,
      )
      if (get().loading.activeRequestId !== activeRequestId) return
      if (!snapshot) return
      get().hydrateSnapshot(snapshot)
    } finally {
      set((s) => ({
        loading: {
          ...s.loading,
          loadingSprintId:
            s.loading.activeRequestId === activeRequestId ? null : s.loading.loadingSprintId,
        },
      }))
    }
  },

  async resyncActiveScopes(reason) {
    const ctx = get().context
    if (!ctx.activeProduct?.id) return
    set((s) => ({
      sync: { ...s.sync, lastResyncAt: Date.now(), resyncReason: reason },
    }))
    await get().ensureWorkspaceLoaded(ctx.activeProduct.id, ctx.activeSprint?.id)
  },
}))
