import type { SupabaseClient, Json } from '../../../supabase'
import type { ExecutionContext } from '../../../execution/engine/base/types'
import type { AgentDefinition } from '../../../execution/engine/agent/core/types'
import type { AgentMemory } from '../../../execution/engine/agent/memory/types'
import type { SessionRow, SessionTurnResult } from '../types'
import { createLLMAdapter } from '../../../execution/engine/llm/adapters/server/adapter-factory'

export interface SessionTurnExecutorResult<TOutput> {
  output: TOutput | null
  memorySnapshot: AgentMemory
}

export type SessionTurnExecutor<TOutput> = (
  session: Session,
  lockedTurnNumber: number
) => Promise<SessionTurnExecutorResult<TOutput>>

export interface SessionTurnExecutionOptions {
  beforeExecute?: (session: Session, lockedTurnNumber: number) => Promise<void>
}

function isMemoryEntry(value: unknown): value is AgentMemory['history'][number] {
  if (!value || typeof value !== 'object' || Array.isArray(value)) return false
  const entry = value as Record<string, unknown>
  return (
    typeof entry.type === 'string' &&
    typeof entry.content === 'string' &&
    typeof entry.timestamp === 'number' &&
    (typeof entry.turnNumber === 'number' || entry.turnNumber === null) &&
    (typeof entry.iterationNumber === 'number' || entry.iterationNumber === null)
  )
}

function isAgentMemory(value: unknown): value is AgentMemory {
  if (!value || typeof value !== 'object' || Array.isArray(value)) return false
  const memory = value as Record<string, unknown>
  const sessionMemory = memory.sessionMemory
  const history = memory.history
  return (
    !!sessionMemory &&
    typeof sessionMemory === 'object' &&
    !Array.isArray(sessionMemory) &&
    Object.values(sessionMemory as Record<string, unknown>).every(isMemoryEntry) &&
    Array.isArray(history) &&
    history.every(isMemoryEntry)
  )
}

export class Session {
  constructor(
    private db: SupabaseClient,
    private data: SessionRow
  ) {}

  // Getters
  get sessionId(): string {
    return this.data.session_id
  }

  get organizationId(): string {
    return this.data.organization_id
  }

  get userId(): string | null {
    return this.data.user_id
  }

  get resourceId(): string {
    return this.data.resource_id
  }

  get totalTurns(): number {
    return this.data.session_total_turns ?? 0
  }

  get memorySnapshot(): AgentMemory {
    return this.data.memory_snapshot as unknown as AgentMemory
  }

  get metadata(): Record<string, unknown> | null {
    return this.data.metadata as Record<string, unknown> | null
  }

  /** Title derived from the first user message, stored in metadata */
  get title(): string | null {
    const meta = this.data.metadata as Record<string, unknown> | null
    return (meta?.title as string) ?? null
  }

  get createdAt(): Date {
    return new Date(this.data.created_at!)
  }

  get updatedAt(): Date {
    return new Date(this.data.updated_at!)
  }

  get isEnded(): boolean {
    return !!this.data.ended_at
  }

  get contextWindowSize(): number {
    return this.data.context_window_size
  }

  /**
   * Execute a turn in this session
   *
   * Uses database transaction with row-level lock to prevent concurrent turn execution.
   * Pattern: Lock → Read → Validate → Execute → Save
   *
   * @param agentDefinition - Agent definition to execute
   * @param input - Input for this turn
   * @param context - Full execution context (including sessionTurnMessages if streaming)
   * @returns Turn result with output and metadata
   */
  async executeTurn<TInput, TOutput>(
    agentDefinition: AgentDefinition,
    input: TInput,
    context: ExecutionContext,
    options?: SessionTurnExecutionOptions
  ): Promise<SessionTurnResult<TOutput>> {
    return this.executeTurnWithExecutor(context, async () => {
      // Create agent instance (stateless, recreated per turn)
      const { Agent } = await import('../../../execution/engine/agent/core/agent')
      const agent = new Agent<TInput, TOutput>(agentDefinition, createLLMAdapter)

      // Execute agent (preloadMemory callback will restore session memory)
      const output = await agent.execute(input, context)

      // Update state and persist
      const memorySnapshot = agent.getMemorySnapshot()
      if (!memorySnapshot) {
        throw new Error('Agent did not produce memory snapshot')
      }

      return { output, memorySnapshot }
    }, options)
  }

  /**
   * Execute a session turn with a caller-provided execution body.
   *
   * Keeps the session lifecycle centralized for both in-process agents and
   * worker-backed external agents: acquire lock, validate active session,
   * execute, persist memory snapshot, and increment turn count exactly once.
   */
  async executeTurnWithExecutor<TOutput>(
    context: ExecutionContext,
    executor: SessionTurnExecutor<TOutput>,
    options?: SessionTurnExecutionOptions
  ): Promise<SessionTurnResult<TOutput>> {
    const turnStartTime = Date.now()

    // NOTE: Using RPC to acquire row-level lock and get fresh state
    // This ensures atomic lock acquisition for concurrent turn prevention
    const { data, error } = await this.db.rpc('execute_session_turn', {
      p_session_id: this.sessionId
    })

    if (error) {
      throw new Error(`Failed to acquire session lock: ${error.message}`)
    }

    // Reload fresh state from lock result
    const lockResult = (Array.isArray(data) ? data[0] : data) as unknown
    if (!lockResult) {
      throw new Error('Session not found')
    }

    this.data = lockResult as SessionRow

    // Validate
    if (this.isEnded) {
      throw new Error('Cannot execute turn on ended session')
    }

    const lockedTurnNumber = (this.data.session_total_turns ?? 0) + 1
    context.sessionTurnNumber = lockedTurnNumber
    await options?.beforeExecute?.(this, lockedTurnNumber)

    const result = await executor(this, lockedTurnNumber)
    if (!isAgentMemory(result.memorySnapshot)) {
      throw new Error('Session turn did not produce memory snapshot')
    }

    // Complete turn (messages already persisted immediately via onMessageEvent)
    await this.completeTurn(result.memorySnapshot)

    return {
      sessionId: this.sessionId,
      executionId: context.executionId,
      lockedTurnNumber,
      turnNumber: this.data.session_total_turns ?? 0,
      output: result.output,
      duration: Date.now() - turnStartTime
    }
  }

  /**
   * End this session (marks as ended, prevents future turns)
   */
  async end(): Promise<void> {
    this.data.ended_at = new Date().toISOString()
    this.data.updated_at = new Date().toISOString()
    await this.save()
  }

  /**
   * Convert to JSON representation
   */
  toJSON(): SessionRow {
    return { ...this.data }
  }

  /**
   * Save current state to database
   */
  private async save(): Promise<void> {
    const { error } = await this.db
      .from('sessions')
      .update({
        memory_snapshot: this.data.memory_snapshot,
        session_total_turns: this.data.session_total_turns,
        updated_at: this.data.updated_at,
        ended_at: this.data.ended_at
      })
      .eq('session_id', this.sessionId)

    if (error) {
      throw new Error(`Failed to save session: ${error.message}`)
    }
  }

  /**
   * Complete turn by updating session state
   * Messages already persisted immediately via onMessageEvent callback
   * @param snapshot - Agent memory snapshot
   */
  private async completeTurn(snapshot: AgentMemory): Promise<void> {
    // Update session state
    this.data.memory_snapshot = snapshot as unknown as Json
    this.data.session_total_turns = (this.data.session_total_turns ?? 0) + 1
    this.data.updated_at = new Date().toISOString()

    // Persist session state (messages already in DB)
    await this.save()
  }
}
