import { mkdir, writeFile, readFile, readdir, rm, access } from 'fs/promises';
import { join } from 'path';
import { simpleGit, type SimpleGit } from 'simple-git';
import type { CoreMessage } from 'ai';
import type {
  Checkpoint,
  CheckpointMetadata,
  RestoreOptions,
  ListCheckpointsOptions,
} from './types.js';
import { createLogger, type Logger } from '../utils/logger.js';

/**
 * Checkpoint Manager
 *
 * Provides git-based session persistence:
 * - Save conversation state with git commit context
 * - Restore previous sessions
 * - Track file modifications per session
 */
export class CheckpointManager {
  private git: SimpleGit;
  private repoRoot: string;
  private checkpointDir: string;
  private logger: Logger;

  constructor(options: { repoRoot: string; logger?: Logger }) {
    this.repoRoot = options.repoRoot;
    this.git = simpleGit(options.repoRoot);
    this.checkpointDir = join(options.repoRoot, '.emdash', 'checkpoints');
    this.logger = options.logger ?? createLogger({ name: 'checkpoint' });
  }

  /**
   * Initialize checkpoint directory
   */
  async init(): Promise<void> {
    await mkdir(this.checkpointDir, { recursive: true });
  }

  /**
   * Create a new checkpoint
   */
  async create(options: {
    sessionId: string;
    messages: CoreMessage[];
    toolCalls?: Checkpoint['toolCalls'];
    description?: string;
    model?: string;
    mode?: 'code' | 'plan' | 'explore';
    state?: Record<string, unknown>;
  }): Promise<CheckpointMetadata> {
    await this.init();

    // Get current git state
    const commitHash = await this.git.revparse(['HEAD']);
    const branch = await this.git.revparse(['--abbrev-ref', 'HEAD']);

    // Get modified files
    const status = await this.git.status();
    const filesModified = [...status.modified, ...status.created, ...status.renamed.map((r) => r.to)];

    // Generate checkpoint ID
    const id = `ckpt-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;

    const checkpoint: Checkpoint = {
      id,
      sessionId: options.sessionId,
      createdAt: new Date().toISOString(),
      commitHash: commitHash.trim(),
      branch: branch.trim(),
      description: options.description,
      model: options.model,
      mode: options.mode,
      messageCount: options.messages.length,
      toolCallCount: options.toolCalls?.length ?? 0,
      filesModified,
      messages: options.messages,
      toolCalls: options.toolCalls ?? [],
      state: options.state,
    };

    // Save checkpoint
    const checkpointPath = join(this.checkpointDir, `${id}.json`);
    await writeFile(checkpointPath, JSON.stringify(checkpoint, null, 2), 'utf-8');

    this.logger.info(
      { checkpointId: id, sessionId: options.sessionId, messageCount: checkpoint.messageCount },
      'Checkpoint created'
    );

    // Return metadata only
    const { messages, toolCalls, state, ...metadata } = checkpoint;
    return metadata;
  }

  /**
   * Load a checkpoint by ID
   */
  async load(checkpointId: string): Promise<Checkpoint | null> {
    const checkpointPath = join(this.checkpointDir, `${checkpointId}.json`);

    try {
      await access(checkpointPath);
      const content = await readFile(checkpointPath, 'utf-8');
      return JSON.parse(content) as Checkpoint;
    } catch {
      return null;
    }
  }

  /**
   * Load the latest checkpoint for a session
   */
  async loadLatest(sessionId: string): Promise<Checkpoint | null> {
    const checkpoints = await this.list({ sessionId, limit: 1, sortOrder: 'desc' });

    if (checkpoints.length === 0) {
      return null;
    }

    return this.load(checkpoints[0].id);
  }

  /**
   * Restore a checkpoint
   */
  async restore(
    checkpointId: string,
    options: RestoreOptions = {}
  ): Promise<{ checkpoint: Checkpoint; gitRestored: boolean }> {
    const checkpoint = await this.load(checkpointId);

    if (!checkpoint) {
      throw new Error(`Checkpoint ${checkpointId} not found`);
    }

    let gitRestored = false;

    if (options.restoreGit) {
      // Check for uncommitted changes
      const status = await this.git.status();
      if (!status.isClean()) {
        throw new Error('Cannot restore git state: uncommitted changes exist. Commit or stash first.');
      }

      if (options.createBranch) {
        // Create a new branch at the checkpoint commit
        await this.git.checkoutBranch(options.createBranch, checkpoint.commitHash);
        this.logger.info(
          { branch: options.createBranch, commit: checkpoint.commitHash },
          'Created branch from checkpoint'
        );
      } else {
        // Checkout the checkpoint commit (detached HEAD)
        await this.git.checkout(checkpoint.commitHash);
        this.logger.info({ commit: checkpoint.commitHash }, 'Checked out checkpoint commit');
      }

      gitRestored = true;
    }

    this.logger.info(
      { checkpointId, sessionId: checkpoint.sessionId, gitRestored },
      'Checkpoint restored'
    );

    return { checkpoint, gitRestored };
  }

  /**
   * List checkpoints
   */
  async list(options: ListCheckpointsOptions = {}): Promise<CheckpointMetadata[]> {
    await this.init();

    try {
      const files = await readdir(this.checkpointDir);
      const checkpoints: CheckpointMetadata[] = [];

      for (const file of files) {
        if (!file.endsWith('.json')) continue;

        try {
          const content = await readFile(join(this.checkpointDir, file), 'utf-8');
          const checkpoint = JSON.parse(content) as Checkpoint;

          // Filter by sessionId if specified
          if (options.sessionId && checkpoint.sessionId !== options.sessionId) {
            continue;
          }

          // Extract metadata
          const { messages, toolCalls, state, ...metadata } = checkpoint;
          checkpoints.push(metadata);
        } catch {
          // Skip invalid files
        }
      }

      // Sort
      const sortBy = options.sortBy ?? 'createdAt';
      const sortOrder = options.sortOrder ?? 'desc';

      checkpoints.sort((a, b) => {
        let comparison = 0;
        if (sortBy === 'createdAt') {
          comparison = new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
        } else if (sortBy === 'messageCount') {
          comparison = a.messageCount - b.messageCount;
        }
        return sortOrder === 'desc' ? -comparison : comparison;
      });

      // Apply limit
      if (options.limit) {
        return checkpoints.slice(0, options.limit);
      }

      return checkpoints;
    } catch {
      return [];
    }
  }

  /**
   * Delete a checkpoint
   */
  async delete(checkpointId: string): Promise<boolean> {
    const checkpointPath = join(this.checkpointDir, `${checkpointId}.json`);

    try {
      await rm(checkpointPath);
      this.logger.info({ checkpointId }, 'Checkpoint deleted');
      return true;
    } catch {
      return false;
    }
  }

  /**
   * Delete all checkpoints for a session
   */
  async deleteSession(sessionId: string): Promise<number> {
    const checkpoints = await this.list({ sessionId });
    let deleted = 0;

    for (const checkpoint of checkpoints) {
      if (await this.delete(checkpoint.id)) {
        deleted++;
      }
    }

    this.logger.info({ sessionId, deleted }, 'Session checkpoints deleted');
    return deleted;
  }

  /**
   * Clean up old checkpoints
   */
  async cleanup(options: {
    maxAge?: number; // Max age in milliseconds
    maxCount?: number; // Max checkpoints per session
    keepSessions?: string[]; // Sessions to keep
  }): Promise<number> {
    const allCheckpoints = await this.list();
    const now = Date.now();
    let deleted = 0;

    // Group by session
    const bySession = new Map<string, CheckpointMetadata[]>();
    for (const checkpoint of allCheckpoints) {
      const list = bySession.get(checkpoint.sessionId) ?? [];
      list.push(checkpoint);
      bySession.set(checkpoint.sessionId, list);
    }

    for (const [sessionId, checkpoints] of bySession) {
      // Skip protected sessions
      if (options.keepSessions?.includes(sessionId)) {
        continue;
      }

      // Sort by date (newest first)
      checkpoints.sort(
        (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
      );

      for (let i = 0; i < checkpoints.length; i++) {
        const checkpoint = checkpoints[i];
        const age = now - new Date(checkpoint.createdAt).getTime();

        // Delete if too old or exceeds count limit
        const tooOld = options.maxAge && age > options.maxAge;
        const exceedsCount = options.maxCount && i >= options.maxCount;

        if (tooOld || exceedsCount) {
          if (await this.delete(checkpoint.id)) {
            deleted++;
          }
        }
      }
    }

    this.logger.info({ deleted }, 'Checkpoint cleanup completed');
    return deleted;
  }

  /**
   * Get diff between current state and checkpoint
   */
  async getDiff(checkpointId: string): Promise<string> {
    const checkpoint = await this.load(checkpointId);

    if (!checkpoint) {
      throw new Error(`Checkpoint ${checkpointId} not found`);
    }

    const diff = await this.git.diff([checkpoint.commitHash, 'HEAD']);
    return diff;
  }
}
