/**
 * Editor operation types and undo history foundation.
 *
 * Defines the discriminated union of all possible block-level operations
 * and a `OperationHistory` class that stores operations and computes their
 * inverses. This is the contract for undo/redo, wired into `Editor.vue` via
 * `OperationShell`.
 */

// ── Time source (injectable for testing) ─────────────────────────────

let timeSource: () => number = () => Date.now()

export function setTimeSource(fn: () => number) {
  timeSource = fn
}

// ── Operation types ──────────────────────────────────────────────────

export interface SplitBlockOp {
  type: "split-block"
  /** Index of the block that was split */
  index: number
  /** ID of the newly created block (the "after" part) */
  splitBlockId: string
  /** Depth shared by both blocks */
  depth: number
  /** Content kept by the original block */
  beforeContent: string
  /** Content that became the new block */
  afterContent: string
  timestamp: number
}

export interface MergeBlockOp {
  type: "merge-block"
  /** Index of the block that was removed */
  index: number
  /** ID of the removed block */
  removedBlockId: string
  /** Depth of the removed block */
  removedBlockDepth: number
  /** Content of the previous block before the merge */
  prevContent: string
  /** Content that was appended */
  appendedContent: string
  timestamp: number
}

export interface InsertBlockOp {
  type: "insert-block"
  /** Index where the block was inserted */
  index: number
  /** ID of the inserted block */
  blockId: string
  /** Content of the inserted block */
  content: string
  /** Depth of the inserted block */
  depth: number
  timestamp: number
}

export interface DeleteBlockOp {
  type: "delete-block"
  /** Index of the deleted block */
  index: number
  /** ID of the deleted block */
  blockId: string
  /** Content of the deleted block */
  content: string
  /** Depth of the deleted block */
  depth: number
  timestamp: number
}

export interface IndentBlockOp {
  type: "indent-block"
  /** Index of the block that was indented */
  index: number
  /** Depth before indentation */
  previousDepth: number
  /** Depths of cascaded child blocks before indentation */
  childrenPreviousDepths?: Array<{ index: number; previousDepth: number }>
  timestamp: number
}

export interface OutdentBlockOp {
  type: "outdent-block"
  /** Index of the block that was outdented */
  index: number
  /** Depth before outdentation */
  previousDepth: number
  /** Depths of cascaded child blocks before outdentation */
  childrenPreviousDepths?: Array<{ index: number; previousDepth: number }>
  timestamp: number
}

export interface SetBlockContentOp {
  type: "set-block-content"
  /** ID of the block whose content changed */
  blockId: string
  /** Content before the change */
  previousContent: string
  /** Content after the change */
  newContent: string
  timestamp: number
}

export interface MoveBlockOp {
  type: "move-block"
  /** Source index of the block before the move */
  fromIndex: number
  /** Target index where the block was moved */
  toIndex: number
  /** ID of the moved block */
  blockId: string
  /** Content of the moved block */
  content: string
  /** Depth of the moved block */
  depth: number
  timestamp: number
}

export type EditorOperation =
  | SplitBlockOp
  | MergeBlockOp
  | InsertBlockOp
  | DeleteBlockOp
  | IndentBlockOp
  | OutdentBlockOp
  | SetBlockContentOp
  | MoveBlockOp

// ── Execute result ───────────────────────────────────────────────────

/**
 * Every operation execution returns its own inverse, enabling undo.
 * The inverse operation, when executed, reverses the original mutation.
 */
export interface ExecuteResult {
  inverse: EditorOperation
}

// ── Inverse computation ──────────────────────────────────────────────

export function computeInverse(op: EditorOperation): EditorOperation {
  const now = timeSource()

  switch (op.type) {
    // Inverse of split is merge: the new block at index+1 is removed,
    // and its content is appended back to the block at index.
    case "split-block":
      return {
        type: "merge-block",
        index: op.index + 1,
        removedBlockId: op.splitBlockId,
        removedBlockDepth: op.depth,
        prevContent: op.beforeContent,
        appendedContent: op.afterContent,
        timestamp: now,
      }

    // Inverse of merge is split: the block at index-1 is split into
    // its original content and the removed block's content.
    case "merge-block":
      return {
        type: "split-block",
        index: op.index - 1,
        splitBlockId: op.removedBlockId,
        depth: op.removedBlockDepth,
        beforeContent: op.prevContent,
        afterContent: op.appendedContent,
        timestamp: now,
      }

    case "insert-block":
      return {
        type: "delete-block",
        index: op.index,
        blockId: op.blockId,
        content: op.content,
        depth: op.depth,
        timestamp: now,
      }

    case "delete-block":
      return {
        type: "insert-block",
        index: op.index,
        blockId: op.blockId,
        content: op.content,
        depth: op.depth,
        timestamp: now,
      }

    // Inverse of indent is outdent; carry children depths through so the
    // undo can restore all cascaded descendants.
    case "indent-block":
      return {
        type: "outdent-block",
        index: op.index,
        previousDepth: op.previousDepth + 1,
        childrenPreviousDepths: op.childrenPreviousDepths?.map((c) => ({
          index: c.index,
          previousDepth: c.previousDepth + 1,
        })),
        timestamp: now,
      }

    case "outdent-block":
      return {
        type: "indent-block",
        index: op.index,
        previousDepth: op.previousDepth - 1,
        childrenPreviousDepths: op.childrenPreviousDepths?.map((c) => ({
          index: c.index,
          previousDepth: c.previousDepth - 1,
        })),
        timestamp: now,
      }

    case "set-block-content":
      return {
        type: "set-block-content",
        blockId: op.blockId,
        previousContent: op.newContent,
        newContent: op.previousContent,
        timestamp: now,
      }

    // Inverse of move is move in the opposite direction.
    case "move-block":
      return {
        type: "move-block",
        fromIndex: op.toIndex,
        toIndex: op.fromIndex,
        blockId: op.blockId,
        content: op.content,
        depth: op.depth,
        timestamp: now,
      }
  }
}

// ── Operation history ────────────────────────────────────────────────

export interface OperationHistory {
  /** Execute an operation, compute its inverse, and push to the stack. */
  execute(op: EditorOperation): ExecuteResult
  /**
   * Undo the most recent operation.
   * Returns the inverse operation (caller should execute it), or null
   * if there is nothing to undo.
   */
  undo(): EditorOperation | null
  /**
   * Redo the most recently undone operation.
   * Returns the operation (caller should execute it), or null if there
   * is nothing to redo.
   */
  redo(): EditorOperation | null
  /** Whether undo is available */
  readonly canUndo: boolean
  /** Whether redo is available */
  readonly canRedo: boolean
  /** Clear all history */
  clear(): void
  /**
   * Peek at the most recent past operation without popping it.
   * Returns null if the history is empty.
   */
  peek(): EditorOperation | null
  /**
   * Replace the most recent past entry's operation with `op`, recompute
   * its inverse, and clear the redo stack. Returns false if the history
   * is empty or the operation type doesn't match the last entry.
   *
   * Does NOT run `execute()` side effects — this is purely a storage-level
   * coalesce. The caller is responsible for checking coalescing criteria
   * (same target, time window, etc.) and for preserving any fields that
   * should not change (e.g. `previousContent` on set-block-content).
   */
  coalesce(op: EditorOperation): boolean
}

interface HistoryEntry {
  operation: EditorOperation
  inverse: EditorOperation
}

export function createOperationHistory(
  maxSize: number = 100,
): OperationHistory {
  const past: HistoryEntry[] = []
  const future: HistoryEntry[] = []

  return {
    get canUndo() {
      return past.length > 0
    },

    get canRedo() {
      return future.length > 0
    },

    execute(op: EditorOperation): ExecuteResult {
      const inverse = computeInverse(op)
      past.push({ operation: op, inverse })
      if (past.length > maxSize) {
        past.shift()
      }
      future.length = 0
      return { inverse }
    },

    undo(): EditorOperation | null {
      if (past.length === 0) return null
      const entry = past.pop() as HistoryEntry
      future.push(entry)
      return entry.inverse
    },

    redo(): EditorOperation | null {
      if (future.length === 0) return null
      const entry = future.pop() as HistoryEntry
      past.push(entry)
      return entry.operation
    },

    clear() {
      past.length = 0
      future.length = 0
    },

    peek(): EditorOperation | null {
      if (past.length === 0) return null
      const entry = past[past.length - 1]
      return entry ? entry.operation : null
    },

    coalesce(op: EditorOperation): boolean {
      if (past.length === 0) return false
      const entry = past[past.length - 1]
      if (!entry) return false
      if (entry.operation.type !== op.type) return false
      entry.operation = op
      entry.inverse = computeInverse(op)
      future.length = 0
      return true
    },
  }
}
