import type { SessionEvent } from '@deepseek-ai/dsh-session/types'
import type {
  ConversationEventInput, ConversationLocation, ConversationLocationData,
  ConversationLocationDataStore, ConversationStepDataMap, ConversationTimelineSnapshot,
  ConversationTurnDataMap, StepLocation, TurnLocation,
} from '../contract/conversation.ts'

interface OwnedLocationData {
  readonly owner: string
  readonly value: unknown
}

/** One Context's previous and next Location-data publication. */
export interface ConversationLocationDataChange {
  readonly owner: string
  readonly previous: ConversationLocationData | null
  readonly next: ConversationLocationData | null
}

class MutableLocationDataStore {
  private entries = new Map<string, OwnedLocationData>()

  get(key: string): unknown {
    return this.entries.get(key)?.value
  }

  remove(owner: string, key: string): boolean {
    const current = this.entries.get(key)
    if (current?.owner !== owner) return false
    this.entries.delete(key)
    return true
  }

  set(owner: string, key: string, value: unknown): boolean {
    const current = this.entries.get(key)
    if (current !== undefined && current.owner !== owner) {
      throw new Error(`conversation Location data "${key}" is already owned by ${current.owner}`)
    }
    if (current?.value === value) return false
    this.entries.set(key, { owner, value })
    return true
  }

  replace(entries: ReadonlyMap<string, OwnedLocationData>): boolean {
    let changed = this.entries.size !== entries.size
    if (!changed) {
      for (const [key, value] of entries) {
        const current = this.entries.get(key)
        if (current?.owner !== value.owner || current.value !== value.value) {
          changed = true
          break
        }
      }
    }
    if (changed) this.entries = new Map(entries)
    return changed
  }
}

interface Coordinates {
  readonly turn?: number
  readonly step?: number
  readonly session?: true
}

interface StepDraft {
  readonly turn: number
  readonly step: number
  firstSeq: number
  start?: SessionEvent<'step/start'>
  end?: SessionEvent<'step/end'>
}

interface TurnDraft {
  readonly turn: number
  firstSeq: number
  start?: SessionEvent<'turn/start'>
  end?: SessionEvent<'turn/end'>
  readonly steps: Map<number, StepDraft>
}

const SESSION_LOCATION = { kind: 'session' } as const
const UNRESOLVED_LOCATION = { kind: 'unresolved' } as const

function payloadCoordinates(event: SessionEvent): Coordinates {
  const data = event.data as unknown as { turn?: unknown; step?: unknown }
  if (data.turn === null) return { session: true }
  const turn = Number.isSafeInteger(data.turn) && (data.turn as number) >= 0
    ? data.turn as number
    : undefined
  const step = Number.isSafeInteger(data.step) && (data.step as number) >= 0
    ? data.step as number
    : undefined
  return { ...turn === undefined ? {} : { turn }, ...step === undefined ? {} : { step } }
}

function sameReferences<T>(left: readonly T[], right: readonly T[]): boolean {
  return left.length === right.length && left.every((value, index) => value === right[index])
}

function sameStep(left: StepLocation | undefined, right: StepLocation): boolean {
  return left !== undefined
    && left.start === right.start && left.end === right.end && left.status === right.status
    && left.data === right.data
}

function sameTurn(left: TurnLocation | undefined, right: TurnLocation): boolean {
  return left !== undefined
    && left.start === right.start && left.end === right.end && left.status === right.status
    && left.data === right.data && sameReferences(left.steps, right.steps)
}

function sameLocation(left: ConversationLocation | undefined, right: ConversationLocation | undefined): boolean {
  if (left === undefined || right === undefined || left.kind !== right.kind) return left === right
  if (left.kind === 'session' || left.kind === 'unresolved') return true
  if (right.kind === 'session' || right.kind === 'unresolved') return false
  if (left.kind === 'turn' || right.kind === 'turn') {
    return left.kind === 'turn' && right.kind === 'turn' && left.turn === right.turn
  }
  return left.turn === right.turn && left.step === right.step
}

/** Session-owned Turn/Step timeline and event-to-Location index. */
export class ConversationLocationIndex {
  private coordinates = new Map<number, Coordinates>()
  private locations = new Map<number, ConversationLocation>()
  private seqsByTurn = new Map<number, Set<number>>()
  private timeline: ConversationTimelineSnapshot = { turnOrder: [], turns: new Map() }
  private readonly turnDataStores = new Map<number, MutableLocationDataStore>()
  private readonly stepDataStores = new Map<string, MutableLocationDataStore>()
  private currentTurn: number | undefined
  private currentStep: number | undefined

  /**
   * Return the current reference-stable timeline.
   * @returns current timeline snapshot.
   */
  snapshot(): ConversationTimelineSnapshot {
    return this.timeline
  }

  /**
   * Replace all Definition-owned Location values while preserving reader identities.
   * @param entries - complete current set of Definition-owned Location values.
   * @returns whether any published Location data changed.
   */
  replaceData(entries: readonly { readonly owner: string; readonly data: ConversationLocationData }[]): boolean {
    const turns = new Map<number, Map<string, OwnedLocationData>>()
    const steps = new Map<string, Map<string, OwnedLocationData>>()
    for (const { owner, data } of entries) {
      const values = data.kind === 'turn'
        ? turns.get(data.turn) ?? new Map<string, OwnedLocationData>()
        : steps.get(stepDataKey(data.turn, requireStep(data))) ?? new Map<string, OwnedLocationData>()
      const current = values.get(data.key)
      if (current !== undefined && current.owner !== owner) {
        throw new Error(`conversation Location data "${data.key}" is already owned by ${current.owner}`)
      }
      values.set(data.key, { owner, value: data.value })
      if (data.kind === 'turn') turns.set(data.turn, values)
      else steps.set(stepDataKey(data.turn, requireStep(data)), values)
    }
    let changed = false
    for (const turn of new Set([...this.turnDataStores.keys(), ...turns.keys()])) {
      changed = this.mutableTurnData(turn).replace(turns.get(turn) ?? new Map()) || changed
    }
    for (const step of new Set([...this.stepDataStores.keys(), ...steps.keys()])) {
      changed = this.mutableStepData(step).replace(steps.get(step) ?? new Map()) || changed
    }
    return changed
  }

  /**
   * Apply changed Context publications without rebuilding Turn/Step membership.
   * @param changes - incremental removals and replacements from published Contexts.
   * @returns whether any published Location data changed.
   */
  applyData(changes: readonly ConversationLocationDataChange[]): boolean {
    let changed = false
    for (const change of changes) {
      const previous = change.previous
      if (previous === null) continue
      changed = this.storeFor(previous).remove(change.owner, previous.key) || changed
    }
    for (const change of changes) {
      const next = change.next
      if (next === null) continue
      changed = this.storeFor(next).set(change.owner, next.key, next.value) || changed
    }
    return changed
  }

  /**
   * Resolve the latest Location for one event.
   * @param event - event already ingested into this index.
   * @returns current Location, falling back to session when it has no Turn/Step affinity.
   */
  locationOf(event: SessionEvent): ConversationLocation {
    return this.locations.get(event.seq) ?? SESSION_LOCATION
  }

  /**
   * Rebuild timeline facts after replace/prepend or a boundary append.
   * @param entries - complete current window in ascending seq order.
   * @returns seqs whose resolved Location changed.
   */
  rebuild(entries: readonly ConversationEventInput[]): ReadonlySet<number> {
    const previousLocations = this.locations
    const turns = new Map<number, TurnDraft>()
    const coordinates = new Map<number, Coordinates>()
    let currentTurn: number | undefined
    let currentStep: number | undefined

    const turnDraft = (turn: number, seq: number): TurnDraft => {
      let draft = turns.get(turn)
      if (draft === undefined) {
        draft = { turn, firstSeq: seq, steps: new Map() }
        turns.set(turn, draft)
      } else {
        draft.firstSeq = Math.min(draft.firstSeq, seq)
      }
      return draft
    }
    const stepDraft = (turn: number, step: number, seq: number): StepDraft => {
      const owner = turnDraft(turn, seq)
      let draft = owner.steps.get(step)
      if (draft === undefined) {
        draft = { turn, step, firstSeq: seq }
        owner.steps.set(step, draft)
      } else {
        draft.firstSeq = Math.min(draft.firstSeq, seq)
      }
      return draft
    }

    for (const { event } of entries) {
      const explicit = payloadCoordinates(event)
      if (event.type === 'turn/start') {
        currentTurn = event.data.turn
        currentStep = undefined
      }
      if (event.type === 'step/start') {
        currentTurn = event.data.turn
        currentStep = event.data.step
      }
      if (explicit.session !== true && explicit.turn !== undefined) {
        if (currentTurn !== explicit.turn) currentStep = undefined
        currentTurn = explicit.turn
        if (explicit.step !== undefined) currentStep = explicit.step
      }
      const turn = explicit.session === true ? undefined : explicit.turn ?? currentTurn
      const step = explicit.session === true || event.type === 'turn/start' || event.type === 'turn/end'
        ? undefined
        : explicit.step ?? (turn === currentTurn ? currentStep : undefined)
      coordinates.set(event.seq, {
        ...turn === undefined ? {} : { turn },
        ...turn === undefined || step === undefined ? {} : { step },
      })
      if (turn !== undefined) turnDraft(turn, event.seq)
      if (turn !== undefined && step !== undefined) stepDraft(turn, step, event.seq)

      if (event.type === 'turn/start') {
        turnDraft(event.data.turn, event.seq).start = event
      } else if (event.type === 'turn/end') {
        turnDraft(event.data.turn, event.seq).end = event
      } else if (event.type === 'step/start') {
        stepDraft(event.data.turn, event.data.step, event.seq).start = event
      } else if (event.type === 'step/end') {
        stepDraft(event.data.turn, event.data.step, event.seq).end = event
      }

      if (event.type === 'step/end' && currentTurn === event.data.turn && currentStep === event.data.step) {
        currentStep = undefined
      }
      if (event.type === 'turn/end' && currentTurn === event.data.turn) {
        currentTurn = undefined
        currentStep = undefined
      }
    }

    const previousTurns = this.timeline.turns
    const nextTurns = new Map<number, TurnLocation>()
    const orderedDrafts = [...turns.values()].sort((left, right) => left.firstSeq - right.firstSeq)
    for (const draft of orderedDrafts) {
      const previousTurn = previousTurns.get(draft.turn)
      const previousSteps = new Map(previousTurn?.steps.map(step => [step.step, step]) ?? [])
      const steps = [...draft.steps.values()]
        .sort((left, right) => left.firstSeq - right.firstSeq)
        .map((candidate): StepLocation => {
          const value: StepLocation = {
            turn: candidate.turn,
            step: candidate.step,
            start: candidate.start,
            end: candidate.end,
            status: candidate.end !== undefined
              ? 'closed'
              : candidate.start === undefined ? 'unknown' : 'open',
            data: this.stepData(candidate.turn, candidate.step),
          }
          const previous = previousSteps.get(candidate.step)
          return sameStep(previous, value) ? previous as StepLocation : value
        })
      const value: TurnLocation = {
        turn: draft.turn,
        start: draft.start,
        end: draft.end,
        status: draft.end !== undefined ? 'closed' : draft.start === undefined ? 'unknown' : 'open',
        steps,
        data: this.turnData(draft.turn),
      }
      nextTurns.set(draft.turn, sameTurn(previousTurn, value) ? previousTurn as TurnLocation : value)
    }

    const nextOrder = orderedDrafts.map(draft => draft.turn)
    const turnOrder = this.timeline.turnOrder.length === nextOrder.length
      && this.timeline.turnOrder.every((turn, index) => turn === nextOrder[index])
      ? this.timeline.turnOrder
      : nextOrder
    let sameMap = previousTurns.size === nextTurns.size
    if (sameMap) {
      for (const [turn, value] of nextTurns) {
        if (previousTurns.get(turn) !== value) {
          sameMap = false
          break
        }
      }
    }
    this.timeline = sameMap && turnOrder === this.timeline.turnOrder
      ? this.timeline
      : { turnOrder, turns: nextTurns }
    this.coordinates = coordinates
    this.locations = new Map()
    this.seqsByTurn = new Map()
    for (const { event } of entries) {
      const coordinates = this.coordinates.get(event.seq)
      if (coordinates?.turn !== undefined) this.indexTurnSeq(coordinates.turn, event.seq)
      this.locations.set(event.seq, this.resolve(event.seq))
    }
    this.currentTurn = currentTurn
    this.currentStep = currentStep

    const changed = new Set<number>()
    for (const { event } of entries) {
      if (!sameLocation(previousLocations.get(event.seq), this.locations.get(event.seq))) {
        changed.add(event.seq)
      }
    }
    return changed
  }

  /**
   * Append one Turn/Step boundary while revisiting only the owning Turn.
   * @param event - contiguous tail boundary event.
   * @returns seqs whose immutable Location reference changed.
   */
  appendBoundary(event: SessionEvent): ReadonlySet<number> {
    if (event.type !== 'turn/start' && event.type !== 'turn/end'
      && event.type !== 'step/start' && event.type !== 'step/end') {
      throw new Error(`conversation Location boundary expected, received ${event.type}`)
    }

    const explicit = payloadCoordinates(event)
    if (event.type === 'turn/start') {
      this.currentTurn = event.data.turn
      this.currentStep = undefined
    } else if (event.type === 'step/start') {
      this.currentTurn = event.data.turn
      this.currentStep = event.data.step
    }
    if (explicit.turn !== undefined) {
      if (this.currentTurn !== explicit.turn) this.currentStep = undefined
      this.currentTurn = explicit.turn
      if (explicit.step !== undefined) this.currentStep = explicit.step
    }
    const turnNumber = explicit.turn ?? this.currentTurn
    if (turnNumber === undefined) throw new Error(`conversation boundary ${event.type} has no turn`)
    const stepNumber = event.type === 'turn/start' || event.type === 'turn/end'
      ? undefined
      : explicit.step ?? (turnNumber === this.currentTurn ? this.currentStep : undefined)
    this.coordinates.set(event.seq, {
      turn: turnNumber,
      ...stepNumber === undefined ? {} : { step: stepNumber },
    })
    this.indexTurnSeq(turnNumber, event.seq)

    const previousTurn = this.timeline.turns.get(turnNumber)
    let steps = previousTurn?.steps ?? []
    if (event.type === 'step/start' || event.type === 'step/end') {
      const number = event.data.step
      const previousStep = steps.find(candidate => candidate.step === number)
      const candidate: StepLocation = {
        turn: turnNumber,
        step: number,
        start: event.type === 'step/start' ? event : previousStep?.start,
        end: event.type === 'step/end' ? event : previousStep?.end,
        status: event.type === 'step/end' || previousStep?.end !== undefined ? 'closed' : 'open',
        data: this.stepData(turnNumber, number),
      }
      const nextStep = sameStep(previousStep, candidate) ? previousStep as StepLocation : candidate
      const index = steps.findIndex(step => step.step === number)
      steps = index < 0
        ? [...steps, nextStep]
        : steps.map((step, at) => at === index ? nextStep : step)
    }
    const candidate: TurnLocation = {
      turn: turnNumber,
      start: event.type === 'turn/start' ? event : previousTurn?.start,
      end: event.type === 'turn/end' ? event : previousTurn?.end,
      status: event.type === 'turn/end' || previousTurn?.end !== undefined
        ? 'closed'
        : event.type === 'turn/start' || previousTurn?.start !== undefined ? 'open' : 'unknown',
      steps,
      data: this.turnData(turnNumber),
    }
    const turn = sameTurn(previousTurn, candidate) ? previousTurn as TurnLocation : candidate
    const turns = new Map(this.timeline.turns)
    turns.set(turnNumber, turn)
    const turnOrder = previousTurn === undefined
      ? [...this.timeline.turnOrder, turnNumber]
      : this.timeline.turnOrder
    this.timeline = { turnOrder, turns }

    const changed = new Set<number>()
    for (const seq of this.seqsByTurn.get(turnNumber) ?? []) {
      const previous = this.locations.get(seq)
      const next = this.resolve(seq)
      this.locations.set(seq, next)
      if (!sameLocation(previous, next)) changed.add(seq)
    }

    if (event.type === 'step/end' && this.currentTurn === event.data.turn && this.currentStep === event.data.step) {
      this.currentStep = undefined
    }
    if (event.type === 'turn/end' && this.currentTurn === event.data.turn) {
      this.currentTurn = undefined
      this.currentStep = undefined
    }
    return changed
  }

  /**
   * Index one non-boundary tail event without rescanning the window.
   * @param event - contiguous appended event.
   */
  appendNonBoundary(event: SessionEvent): void {
    const explicit = payloadCoordinates(event)
    if (explicit.session === true) {
      this.coordinates.set(event.seq, {})
      this.locations.set(event.seq, SESSION_LOCATION)
      return
    }
    if (explicit.turn !== undefined) {
      if (this.currentTurn !== explicit.turn) this.currentStep = undefined
      this.currentTurn = explicit.turn
      if (explicit.step !== undefined) this.currentStep = explicit.step
    }
    const turn = explicit.turn ?? this.currentTurn
    const step = explicit.step ?? (turn === this.currentTurn ? this.currentStep : undefined)
    this.coordinates.set(event.seq, {
      ...turn === undefined ? {} : { turn },
      ...turn === undefined || step === undefined ? {} : { step },
    })
    if (turn !== undefined) this.indexTurnSeq(turn, event.seq)
    this.locations.set(event.seq, this.resolve(event.seq))
  }

  private indexTurnSeq(turn: number, seq: number): void {
    const current = this.seqsByTurn.get(turn) ?? new Set<number>()
    current.add(seq)
    this.seqsByTurn.set(turn, current)
  }

  private turnData(turn: number): ConversationLocationDataStore<ConversationTurnDataMap> {
    return this.mutableTurnData(turn) as ConversationLocationDataStore<ConversationTurnDataMap>
  }

  private stepData(turn: number, step: number): ConversationLocationDataStore<ConversationStepDataMap> {
    return this.mutableStepData(stepDataKey(turn, step)) as ConversationLocationDataStore<ConversationStepDataMap>
  }

  private mutableTurnData(turn: number): MutableLocationDataStore {
    const current = this.turnDataStores.get(turn) ?? new MutableLocationDataStore()
    this.turnDataStores.set(turn, current)
    return current
  }

  private mutableStepData(key: string): MutableLocationDataStore {
    const current = this.stepDataStores.get(key) ?? new MutableLocationDataStore()
    this.stepDataStores.set(key, current)
    return current
  }

  private storeFor(data: ConversationLocationData): MutableLocationDataStore {
    return data.kind === 'turn'
      ? this.mutableTurnData(data.turn)
      : this.mutableStepData(stepDataKey(data.turn, requireStep(data)))
  }

  private resolve(seq: number): ConversationLocation {
    const coordinates = this.coordinates.get(seq)
    if (coordinates?.turn === undefined) return SESSION_LOCATION
    const turn = this.timeline.turns.get(coordinates.turn)
    if (turn === undefined) return UNRESOLVED_LOCATION
    if (coordinates.step === undefined) return { kind: 'turn', turn }
    const step = turn.steps.find(candidate => candidate.step === coordinates.step)
    return step === undefined ? { kind: 'turn', turn } : { kind: 'step', turn, step }
  }
}

function stepDataKey(turn: number, step: number): string {
  return `${turn}:${step}`
}

function requireStep(data: ConversationLocationData): number {
  if (data.kind === 'step' && data.step !== undefined) return data.step
  throw new Error(`conversation Step data "${data.key}" requires a step`)
}
