/**
 * Session Service - Database-backed implementation using Drizzle ORM.
 *
 * Provides CRUD operations for agent sessions with:
 * - Type-safe database queries
 * - Engine-agnostic session configuration storage
 * - JSON config and management info caching
 */
import { randomUUID } from 'node:crypto';
import { eq, desc, and, asc } from 'drizzle-orm';
import { getDb, sessions, messages, type SessionRow } from './db';
import type { EngineName } from './engines/types';
import { sanitizeAgentMessageForPublicRead } from './public-message-sanitizer';

// ============================================================
// Types
// ============================================================

/**
 * System prompt configuration options.
 */
export type SystemPromptConfig =
  | { type: 'custom'; text: string }
  | { type: 'preset'; preset: 'claude_code'; append?: string };

/**
 * Tools configuration - can be a list of tool names or a preset.
 */
export type ToolsConfig = string[] | { type: 'preset'; preset: 'claude_code' };

/**
 * Session options configuration (stored as JSON).
 */
export interface SessionOptionsConfig {
  settingSources?: string[];
  allowedTools?: string[];
  disallowedTools?: string[];
  tools?: ToolsConfig;
  betas?: string[];
  maxThinkingTokens?: number;
  maxTurns?: number;
  maxBudgetUsd?: number;
  mcpServers?: Record<string, unknown>;
  outputFormat?: Record<string, unknown>;
  enableFileCheckpointing?: boolean;
  sandbox?: Record<string, unknown>;
  env?: Record<string, string>;
  /**
   * Optional Codex-specific configuration overrides.
   * Only applicable when using CodexEngine.
   */
  codexConfig?: Partial<import('webpage-mcp-shared').CodexEngineConfig>;
}

/**
 * Cached management information from Claude SDK.
 */
export interface ManagementInfo {
  models?: Array<{ value: string; displayName: string; description: string }>;
  commands?: Array<{ name: string; description: string; argumentHint: string }>;
  account?: { email?: string; organization?: string; subscriptionType?: string };
  mcpServers?: Array<{ name: string; status: string }>;
  tools?: string[];
  agents?: string[];
  /** Plugins with name and path (SDK returns { name, path }[]) */
  plugins?: Array<{ name: string; path?: string }>;
  skills?: string[];
  slashCommands?: string[];
  model?: string;
  permissionMode?: string;
  cwd?: string;
  outputStyle?: string;
  betas?: string[];
  claudeCodeVersion?: string;
  apiKeySource?: string;
  lastUpdated?: string;
}

/**
 * Structured preview metadata for session list display.
 * When present, allows rendering special styles (e.g., chip for web editor apply).
 */
export interface AgentSessionPreviewMeta {
  /** Compact display text (e.g., user's message or "Apply changes") */
  displayText?: string;
  /** Client metadata for special rendering */
  clientMeta?: {
    kind?: 'web_editor_apply_batch' | 'web_editor_apply_single';
    pageUrl?: string | null;
    pageUrlRedacted?: boolean;
    elementCount?: number;
    elementLabels?: string[];
  };
  /** Full content for tooltip preview (truncated to avoid payload bloat) */
  fullContent?: string;
}

/**
 * Agent session representation.
 */
export interface AgentSession {
  id: string;
  projectId: string;
  engineName: string;
  engineSessionId?: string;
  name?: string;
  /** Preview text from first user message, for display in session list */
  preview?: string;
  /** Structured preview metadata for special rendering (e.g., web editor apply chip) */
  previewMeta?: AgentSessionPreviewMeta;
  model?: string;
  permissionMode: string;
  allowDangerouslySkipPermissions: boolean;
  systemPromptConfig?: SystemPromptConfig;
  optionsConfig?: SessionOptionsConfig;
  managementInfo?: ManagementInfo;
  createdAt: string;
  updatedAt: string;
}

/**
 * Options for creating a new session.
 */
export interface CreateSessionOptions {
  id?: string;
  engineSessionId?: string;
  name?: string;
  model?: string;
  permissionMode?: string;
  allowDangerouslySkipPermissions?: boolean;
  systemPromptConfig?: SystemPromptConfig;
  optionsConfig?: SessionOptionsConfig;
}

/**
 * Options for updating an existing session.
 */
export interface UpdateSessionInput {
  engineSessionId?: string | null;
  name?: string | null;
  model?: string | null;
  permissionMode?: string | null;
  allowDangerouslySkipPermissions?: boolean | null;
  systemPromptConfig?: SystemPromptConfig | null;
  optionsConfig?: SessionOptionsConfig | null;
  managementInfo?: ManagementInfo | null;
}

// ============================================================
// JSON Parsing Utilities
// ============================================================

function parseJson<T>(value: string | null): T | undefined {
  if (!value) return undefined;
  try {
    return JSON.parse(value) as T;
  } catch {
    return undefined;
  }
}

function stringifyJson<T>(value: T | null | undefined): string | null {
  if (value === null || value === undefined) return null;
  return JSON.stringify(value);
}

// ============================================================
// Type Conversion
// ============================================================

function rowToSession(row: SessionRow): AgentSession {
  return {
    id: row.id,
    projectId: row.projectId,
    engineName: row.engineName,
    engineSessionId: row.engineSessionId ?? undefined,
    name: row.name ?? undefined,
    model: row.model ?? undefined,
    permissionMode: row.permissionMode,
    allowDangerouslySkipPermissions: row.allowDangerouslySkipPermissions === '1',
    systemPromptConfig: parseJson<SystemPromptConfig>(row.systemPromptConfig),
    optionsConfig: parseJson<SessionOptionsConfig>(row.optionsConfig),
    managementInfo: parseJson<ManagementInfo>(row.managementInfo),
    createdAt: row.createdAt,
    updatedAt: row.updatedAt,
  };
}

// ============================================================
// Public API
// ============================================================

/**
 * Create a new session for a project.
 */
export async function createSession(
  projectId: string,
  engineName: EngineName,
  options: CreateSessionOptions = {},
): Promise<AgentSession> {
  const db = getDb();
  const now = new Date().toISOString();

  // Resolve permission mode - AgentChat defaults to bypassPermissions for headless operation
  const resolvedPermissionMode = options.permissionMode?.trim() || 'bypassPermissions';

  // SDK requires allowDangerouslySkipPermissions=true when using bypassPermissions mode
  // If explicitly provided, use that value; otherwise infer from permission mode
  const resolvedAllowDangerouslySkipPermissions =
    typeof options.allowDangerouslySkipPermissions === 'boolean'
      ? options.allowDangerouslySkipPermissions
      : resolvedPermissionMode === 'bypassPermissions';

  const sessionData = {
    id: options.id?.trim() || randomUUID(),
    projectId,
    engineName,
    engineSessionId: options.engineSessionId?.trim() || null,
    name: options.name?.trim() || null,
    model: options.model?.trim() || null,
    permissionMode: resolvedPermissionMode,
    allowDangerouslySkipPermissions: resolvedAllowDangerouslySkipPermissions ? '1' : null,
    systemPromptConfig: stringifyJson(options.systemPromptConfig),
    optionsConfig: stringifyJson(options.optionsConfig),
    managementInfo: null,
    createdAt: now,
    updatedAt: now,
  };

  await db.insert(sessions).values(sessionData);
  return rowToSession(sessionData as SessionRow);
}

/**
 * Get a session by ID.
 */
export async function getSession(sessionId: string): Promise<AgentSession | undefined> {
  const db = getDb();
  const rows = await db.select().from(sessions).where(eq(sessions.id, sessionId)).limit(1);
  return rows.length > 0 ? rowToSession(rows[0]) : undefined;
}

/** Maximum length for preview text */
const MAX_PREVIEW_LENGTH = 50;

/**
 * Truncate text to max length with ellipsis.
 */
function truncatePreview(text: string, maxLength: number = MAX_PREVIEW_LENGTH): string {
  const trimmed = text.trim().replace(/\s+/g, ' ');
  if (trimmed.length <= maxLength) return trimmed;
  return trimmed.slice(0, maxLength - 1) + '…';
}

/**
 * Add preview to sessions by fetching first user message for each.
 * Shared helper to avoid code duplication.
 */
async function addPreviewsToSessions(rows: SessionRow[]): Promise<AgentSession[]> {
  const db = getDb();

  return Promise.all(
    rows.map(async (row) => {
      const session = rowToSession(row);

      // Query first user message for this session (include metadata for special rendering)
      const firstUserMessages = await db
        .select({ content: messages.content, metadata: messages.metadata })
        .from(messages)
        .where(
          and(
            eq(messages.sessionId, row.id),
            eq(messages.projectId, row.projectId),
            eq(messages.role, 'user'),
          ),
        )
        .orderBy(asc(messages.createdAt))
        .limit(1);

      if (firstUserMessages.length > 0 && firstUserMessages[0].content) {
        const content = firstUserMessages[0].content;
        const metadataJson = firstUserMessages[0].metadata;
        let previewContent = content;
        let previewMetadata: Record<string, unknown> | undefined;

        if (metadataJson && metadataJson.trim().length > 0) {
          try {
            const parsedMetadata = JSON.parse(metadataJson) as Record<string, unknown>;
            const sanitized = sanitizeAgentMessageForPublicRead({
              content,
              metadata: parsedMetadata,
            });
            previewContent = sanitized.content;
            previewMetadata = sanitized.metadata;
          } catch {
            // Ignore JSON parse errors, just use plain preview
          }
        }

        session.preview = truncatePreview(previewContent);

        // Parse metadata to extract clientMeta/displayText for special rendering
        if (previewMetadata) {
          try {
            // Type-safe extraction with validation
            const rawClientMeta = previewMetadata.clientMeta;
            const rawDisplayText = previewMetadata.displayText;

            // Validate displayText is a string
            const displayText = typeof rawDisplayText === 'string' ? rawDisplayText : undefined;

            // Validate clientMeta structure
            const clientMeta =
              rawClientMeta &&
              typeof rawClientMeta === 'object' &&
              'kind' in rawClientMeta &&
              (rawClientMeta.kind === 'web_editor_apply_batch' ||
                rawClientMeta.kind === 'web_editor_apply_single')
                ? (rawClientMeta as AgentSessionPreviewMeta['clientMeta'])
                : undefined;

            // Only set previewMeta if we have valid special metadata
            if (clientMeta || displayText) {
              session.previewMeta = {
                displayText: displayText || truncatePreview(previewContent),
                clientMeta,
                // Truncate fullContent to avoid payload bloat (200 chars max)
                fullContent: truncatePreview(previewContent, 200),
              };
            }
          } catch {
            // Ignore unexpected metadata shapes, just use plain preview
          }
        }
      }

      return session;
    }),
  );
}

/**
 * Get all sessions for a project, sorted by most recently updated.
 * Includes preview from first user message for each session.
 */
export async function getSessionsByProject(projectId: string): Promise<AgentSession[]> {
  const db = getDb();
  const rows = await db
    .select()
    .from(sessions)
    .where(eq(sessions.projectId, projectId))
    .orderBy(desc(sessions.updatedAt));

  return addPreviewsToSessions(rows);
}

/**
 * Get all sessions across all projects, sorted by most recently updated.
 * Includes preview from first user message for each session.
 */
export async function getAllSessions(): Promise<AgentSession[]> {
  const db = getDb();
  const rows = await db.select().from(sessions).orderBy(desc(sessions.updatedAt));

  return addPreviewsToSessions(rows);
}

/**
 * Get sessions for a project filtered by engine name.
 */
export async function getSessionsByProjectAndEngine(
  projectId: string,
  engineName: EngineName,
): Promise<AgentSession[]> {
  const db = getDb();
  const rows = await db
    .select()
    .from(sessions)
    .where(and(eq(sessions.projectId, projectId), eq(sessions.engineName, engineName)))
    .orderBy(desc(sessions.updatedAt));
  return rows.map(rowToSession);
}

/**
 * Update an existing session.
 */
export async function updateSession(sessionId: string, updates: UpdateSessionInput): Promise<void> {
  const db = getDb();
  const now = new Date().toISOString();

  const updateData: Record<string, unknown> = {
    updatedAt: now,
  };

  if (updates.engineSessionId !== undefined) {
    updateData.engineSessionId = updates.engineSessionId?.trim() || null;
  }

  if (updates.name !== undefined) {
    updateData.name = updates.name?.trim() || null;
  }

  if (updates.model !== undefined) {
    updateData.model = updates.model?.trim() || null;
  }

  if (updates.permissionMode !== undefined) {
    updateData.permissionMode = updates.permissionMode?.trim() || 'bypassPermissions';
  }

  if (updates.allowDangerouslySkipPermissions !== undefined) {
    updateData.allowDangerouslySkipPermissions = updates.allowDangerouslySkipPermissions
      ? '1'
      : null;
  }

  if (updates.systemPromptConfig !== undefined) {
    updateData.systemPromptConfig = stringifyJson(updates.systemPromptConfig);
  }

  if (updates.optionsConfig !== undefined) {
    updateData.optionsConfig = stringifyJson(updates.optionsConfig);
  }

  if (updates.managementInfo !== undefined) {
    updateData.managementInfo = stringifyJson(updates.managementInfo);
  }

  await db.update(sessions).set(updateData).where(eq(sessions.id, sessionId));
}

/**
 * Update the engine binding for an existing legacy session.
 * Clears engine-specific state because it is not portable across runtimes.
 */
export async function updateSessionEngineName(
  sessionId: string,
  engineName: EngineName,
): Promise<void> {
  const db = getDb();
  const now = new Date().toISOString();
  await db
    .update(sessions)
    .set({
      engineName,
      engineSessionId: null,
      model: null,
      updatedAt: now,
    })
    .where(eq(sessions.id, sessionId));
}

/**
 * Delete a session by ID.
 */
export async function deleteSession(sessionId: string): Promise<void> {
  const db = getDb();
  const existing = await db
    .select({ projectId: sessions.projectId })
    .from(sessions)
    .where(eq(sessions.id, sessionId))
    .limit(1);

  if (existing.length === 0) {
    return;
  }

  await db
    .delete(messages)
    .where(and(eq(messages.sessionId, sessionId), eq(messages.projectId, existing[0].projectId)));
  await db.delete(sessions).where(eq(sessions.id, sessionId));
}

/**
 * Update the engine session ID (e.g., Claude SDK session_id).
 */
export async function updateEngineSessionId(
  sessionId: string,
  engineSessionId: string | null,
): Promise<void> {
  await updateSession(sessionId, { engineSessionId });
}

/**
 * Touch session activity - updates the updatedAt timestamp.
 * Used when a message is sent to move the session to the top of the list.
 */
export async function touchSessionActivity(sessionId: string): Promise<void> {
  const db = getDb();
  const now = new Date().toISOString();
  await db.update(sessions).set({ updatedAt: now }).where(eq(sessions.id, sessionId));
}

/**
 * Update the cached management information.
 */
export async function updateManagementInfo(
  sessionId: string,
  info: ManagementInfo | null,
): Promise<void> {
  // Add timestamp to management info
  const infoWithTimestamp = info ? { ...info, lastUpdated: new Date().toISOString() } : null;
  await updateSession(sessionId, { managementInfo: infoWithTimestamp });
}

/**
 * Get or create a default session for a project and engine.
 * Useful for backwards compatibility - creates a session if none exists.
 */
export async function getOrCreateDefaultSession(
  projectId: string,
  engineName: EngineName,
  options: CreateSessionOptions = {},
): Promise<AgentSession> {
  const existingSessions = await getSessionsByProjectAndEngine(projectId, engineName);

  if (existingSessions.length > 0) {
    // Return the most recently updated session
    return existingSessions[0];
  }

  // Create a new default session
  return createSession(projectId, engineName, {
    ...options,
    name: options.name || `Default ${engineName} session`,
  });
}
