import { readFile, writeFile, readdir, stat } from 'fs/promises';
import { join, relative, extname } from 'path';
import type { EmbeddingProvider, EmbeddingResult, SearchResult } from './types.js';
import { InMemoryEmbeddingStore } from './store.js';
import { OpenAIEmbeddingProvider } from './providers/openai.js';
import { FireworksEmbeddingProvider } from './providers/fireworks.js';

export type ProviderType = 'openai' | 'fireworks';

export interface EmbeddingServiceOptions {
  provider?: ProviderType;
  repoRoot: string;
  cacheDir?: string;
}

export interface IndexOptions {
  extensions?: string[];
  ignore?: string[];
  maxFileSize?: number;
  chunkSize?: number;
  chunkOverlap?: number;
}

const DEFAULT_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.py', '.go', '.rs', '.java', '.md'];
const DEFAULT_IGNORE = ['node_modules', '.git', 'dist', 'build', '.next', 'coverage', '__pycache__'];
const DEFAULT_MAX_FILE_SIZE = 100 * 1024; // 100KB
const DEFAULT_CHUNK_SIZE = 1000; // characters
const DEFAULT_CHUNK_OVERLAP = 200; // characters

/**
 * Embedding service for semantic code search
 *
 * Provides indexing and search capabilities using embeddings.
 */
export class EmbeddingService {
  private provider: EmbeddingProvider;
  private store: InMemoryEmbeddingStore;
  private repoRoot: string;
  private cacheDir: string;

  constructor(options: EmbeddingServiceOptions) {
    this.repoRoot = options.repoRoot;
    this.cacheDir = options.cacheDir ?? join(options.repoRoot, '.emdash', 'embeddings');
    this.store = new InMemoryEmbeddingStore();
    this.provider = this.createProvider(options.provider ?? 'openai');
  }

  private createProvider(type: ProviderType): EmbeddingProvider {
    switch (type) {
      case 'openai':
        return new OpenAIEmbeddingProvider();
      case 'fireworks':
        return new FireworksEmbeddingProvider();
      default:
        throw new Error(`Unknown embedding provider: ${type}`);
    }
  }

  /**
   * Index files in the repository
   */
  async indexRepository(options: IndexOptions = {}): Promise<{
    filesIndexed: number;
    chunksIndexed: number;
  }> {
    const extensions = options.extensions ?? DEFAULT_EXTENSIONS;
    const ignore = options.ignore ?? DEFAULT_IGNORE;
    const maxFileSize = options.maxFileSize ?? DEFAULT_MAX_FILE_SIZE;
    const chunkSize = options.chunkSize ?? DEFAULT_CHUNK_SIZE;
    const chunkOverlap = options.chunkOverlap ?? DEFAULT_CHUNK_OVERLAP;

    // Clear existing index
    this.store.clear();

    // Find all files
    const files = await this.findFiles(this.repoRoot, extensions, ignore);

    let filesIndexed = 0;
    let chunksIndexed = 0;

    // Process files in batches
    const batchSize = 10;
    for (let i = 0; i < files.length; i += batchSize) {
      const batch = files.slice(i, i + batchSize);
      const chunks: { text: string; file: string; start: number; end: number }[] = [];

      for (const file of batch) {
        try {
          const fileStat = await stat(file);
          if (fileStat.size > maxFileSize) continue;

          const content = await readFile(file, 'utf-8');
          const relativePath = relative(this.repoRoot, file);

          // Split into chunks
          const fileChunks = this.chunkText(content, chunkSize, chunkOverlap);
          for (const chunk of fileChunks) {
            chunks.push({
              text: chunk.text,
              file: relativePath,
              start: chunk.start,
              end: chunk.end,
            });
          }

          filesIndexed++;
        } catch {
          // Skip files that can't be read
        }
      }

      if (chunks.length > 0) {
        // Get embeddings for chunks
        const texts = chunks.map((c) => c.text);
        const embeddings = await this.provider.embed(texts);

        // Add to store
        const results: EmbeddingResult[] = chunks.map((chunk, idx) => ({
          text: chunk.text,
          embedding: embeddings[idx],
          metadata: {
            file: chunk.file,
            start: chunk.start,
            end: chunk.end,
          },
        }));

        this.store.add(results);
        chunksIndexed += results.length;
      }
    }

    return { filesIndexed, chunksIndexed };
  }

  /**
   * Search for similar code/text
   */
  async search(query: string, topK = 10, threshold = 0.5): Promise<SearchResult[]> {
    const queryEmbedding = await this.provider.embedSingle(query);
    return this.store.search(queryEmbedding, topK, threshold);
  }

  /**
   * Get the number of indexed chunks
   */
  getIndexSize(): number {
    return this.store.size();
  }

  /**
   * Save the index to disk
   */
  async saveIndex(): Promise<void> {
    const { mkdir } = await import('fs/promises');
    await mkdir(this.cacheDir, { recursive: true });

    const indexPath = join(this.cacheDir, 'index.json');
    const data = {
      provider: this.provider.name,
      model: this.provider.model,
      items: this.store.getAll(),
    };

    await writeFile(indexPath, JSON.stringify(data), 'utf-8');
  }

  /**
   * Load the index from disk
   */
  async loadIndex(): Promise<boolean> {
    try {
      const indexPath = join(this.cacheDir, 'index.json');
      const content = await readFile(indexPath, 'utf-8');
      const data = JSON.parse(content);

      // Verify provider matches
      if (data.provider !== this.provider.name) {
        console.warn('Index provider mismatch, re-indexing required');
        return false;
      }

      this.store.load(data.items);
      return true;
    } catch {
      return false;
    }
  }

  /**
   * Find files matching extensions
   */
  private async findFiles(
    dir: string,
    extensions: string[],
    ignore: string[]
  ): Promise<string[]> {
    const files: string[] = [];

    async function walk(currentDir: string): Promise<void> {
      const entries = await readdir(currentDir, { withFileTypes: true });

      for (const entry of entries) {
        const name = entry.name;
        const fullPath = join(currentDir, name);

        // Skip ignored directories
        if (ignore.includes(name)) continue;

        if (entry.isDirectory()) {
          await walk(fullPath);
        } else if (entry.isFile()) {
          const ext = extname(name);
          if (extensions.includes(ext)) {
            files.push(fullPath);
          }
        }
      }
    }

    await walk(dir);
    return files;
  }

  /**
   * Split text into overlapping chunks
   */
  private chunkText(
    text: string,
    chunkSize: number,
    overlap: number
  ): { text: string; start: number; end: number }[] {
    const chunks: { text: string; start: number; end: number }[] = [];
    let start = 0;

    while (start < text.length) {
      const end = Math.min(start + chunkSize, text.length);
      const chunkText = text.slice(start, end);

      // Don't add very short chunks at the end
      if (chunkText.length >= 50) {
        chunks.push({ text: chunkText, start, end });
      }

      start += chunkSize - overlap;
    }

    return chunks;
  }
}
