/**
 * SessionManager - Open and close sessions per DD §6.2
 *
 * Implements SessionManager contract (contracts §8.6).
 * - open_session: computes db_path, opens+migrates, ingests session.created
 * - close_session: flushes ui_state, releases handle
 * - Provider/model fixed at open (immutable per session)
 *
 * @module packages/runtime/src/sessions/SessionManager
 */

import * as fs from 'fs'
import * as path from 'path'
import { randomUUID } from 'crypto'

import type {
  ProjectContext,
  SessionContext,
  OpenSessionOptions,
  SessionManager as ISessionManager,
  SessionID,
  ProviderID,
  ModelID,
  ISOTimeString,
  RuntimeEvent,
} from '@aircoding/contracts'

import { DatabaseManager } from '../storage/DatabaseManager.js'
import { MigrationRunner } from '../storage/MigrationRunner.js'
import { EventIngestor } from '../events/EventIngestor.js'

/**
 * SessionManager implements SessionManager contract per DD §6.2.
 *
 * open_session flow:
 * 1. resolve session_id (options or IdGenerator.session_id())
 * 2. compute db_path = .air/local/sessions/<session-id>/session.db
 * 3. DatabaseManager.open(db_path); MigrationRunner.migrate(db)
 * 4. SessionStore bound to this db
 * 5. ingest session.created (durable → inserts sessions row)
 * 6. return SessionContext { session_id, project_id, project_root, db_path, artifact_root }
 *
 * close_session flow:
 * 1. flush ui_state (db-schema §1)
 * 2. publish terminal session event when archiving
 * 3. release DB handle
 */
export class SessionManager implements ISessionManager {
  private dbManager: DatabaseManager
  private migrationRunner: MigrationRunner
  private eventIngestor: EventIngestor
  private openSessions: Map<string, SessionContext> = new Map()

  constructor(eventIngestor?: EventIngestor) {
    this.dbManager = new DatabaseManager()
    this.migrationRunner = new MigrationRunner()
    this.eventIngestor = eventIngestor ?? new EventIngestor()
  }

  /**
   * Open a new session for the given project.
   * Computes db_path, opens+migrates database, ingests session.created event.
   * Provider/model selection is captured at open and immutable for the session.
   */
  async open_session(
    project: ProjectContext,
    options?: OpenSessionOptions
  ): Promise<SessionContext> {
    // 1. Resolve session_id
    const sessionId = this.resolveSessionId(options)

    // 2. Compute db_path = .air/local/sessions/<session-id>/session.db
    const sessionsDir = path.join(project.local_root, 'sessions', sessionId)
    const dbPath = path.join(sessionsDir, 'session.db')

    // 3. Create session directory and open database
    fs.mkdirSync(path.dirname(dbPath), { recursive: true })
    this.dbManager.open(dbPath)

    // Run migrations using raw database
    const db = this.dbManager.getRawDatabase()
    if (db) {
      await this.migrationRunner.migrate(db)
    }

    // 4. Ingest session.created event (durable → inserts sessions row)
    await this.ingestSessionCreated(sessionId, project, options)

    // 5. Compute artifact_root
    const artifactRoot = path.join(sessionsDir, 'artifacts')
    fs.mkdirSync(artifactRoot, { recursive: true })

    // 6. Build and return SessionContext
    const sessionContext: SessionContext = {
      session_id: sessionId,
      project_id: project.project_id,
      project_root: project.project_root,
      db_path: dbPath,
      artifact_root: artifactRoot,
    }

    // Track open session
    this.openSessions.set(sessionId, sessionContext)

    return sessionContext
  }

  /**
   * Close a session - flushes ui_state and releases the DB handle.
   * If archiving, publishes a terminal session event.
   */
  async close_session(sessionId: SessionID): Promise<void> {
    const session = this.openSessions.get(sessionId)
    if (!session) {
      throw new Error(`Session ${sessionId} is not open`)
    }

    // Close database connection
    this.dbManager.close()

    // Remove from tracked sessions
    this.openSessions.delete(sessionId)
  }

  /**
   * Resolve session_id from options or generate a new one.
   */
  private resolveSessionId(options?: OpenSessionOptions): SessionID {
    if (options?.session_id) {
      return options.session_id
    }
    // Generate new session_id (format: sess_<ulid>)
    return `sess_${randomUUID().replace(/-/g, '').slice(0, 24)}` as SessionID
  }

  /**
   * Ingest the session.created event (durable).
   */
  private async ingestSessionCreated(
    sessionId: SessionID,
    project: ProjectContext,
    options?: OpenSessionOptions
  ): Promise<void> {
    const now = new Date().toISOString() as ISOTimeString

    // Build session.created event payload per event-registry §3.1
    const payload = {
      session_id: sessionId,
      project_id: project.project_id,
      project_root: project.project_root,
      title: options?.title,
      model_provider_id: options?.model_provider_id as ProviderID | undefined,
      model_id: options?.model_id as ModelID | undefined,
      metadata: undefined,
    }

    const event: RuntimeEvent<typeof payload> = {
      id: `evt_${randomUUID().replace(/-/g, '').slice(0, 24)}` as any,
      type: 'session.created',
      version: 1,
      timestamp: now,
      session_id: sessionId,
      project_id: project.project_id as any,
      source: {
        kind: 'main',
      },
      route: ['session', 'created'],
      payload,
    }

    try {
      await this.eventIngestor.ingest(event)
    } catch (error) {
      // If event ingestion fails, close the DB and propagate
      this.dbManager.close()
      throw error
    }
  }

  /**
   * Get the current session context if the session is open.
   */
  getSession(sessionId: SessionID): SessionContext | undefined {
    return this.openSessions.get(sessionId)
  }

  /**
   * Check if a session is currently open.
   */
  isSessionOpen(sessionId: SessionID): boolean {
    return this.openSessions.has(sessionId)
  }

  /**
   * Get all open session IDs.
   */
  getOpenSessions(): SessionID[] {
    return Array.from(this.openSessions.keys())
  }
}

/**
 * Creates a new SessionManager instance.
 */
export function createSessionManager(eventIngestor?: EventIngestor): SessionManager {
  return new SessionManager(eventIngestor)
}