/**
 * Folder index session orchestrator.
 *
 * Walks a folder via scanner.ts, sends batches over WS to the backend, applies
 * returned summaries to projectFolderFiles, drives progress callbacks.
 */
import { readFile } from 'node:fs/promises';
import path from 'node:path';
import { randomUUID } from 'node:crypto';
import { getDb } from '../db';
import { projects, projectFolderFiles } from '../db/schema';
import { eq, and } from 'drizzle-orm';
import { scanFolder, type ScannedFile } from './scanner';
import { INDEX_BATCH_SIZE } from './constants';
import { getBackendClient } from '../api/backend-client';

export interface IndexProgress {
  sessionId: string;
  processed: number;
  total: number;
  status: 'starting' | 'scanning' | 'cancelled' | 'completed' | 'quota_exceeded' | 'error';
  error?: string;
}

export type ProgressListener = (p: IndexProgress) => void;

async function readForIndex(
  folderPath: string,
  f: ScannedFile,
): Promise<{ content: string; mime?: string }> {
  const abs = path.join(folderPath, f.relativePath);
  if (f.kind === 'image') {
    const buf = await readFile(abs);
    const ext = f.ext.toLowerCase();
    const mime =
      ext === '.png' ? 'image/png' : ext === '.webp' ? 'image/webp' : 'image/jpeg';
    return { content: buf.toString('base64'), mime };
  }
  if (f.kind === 'text') {
    return { content: await readFile(abs, 'utf-8') };
  }
  // pdf / docx: read as binary, base64. Server is responsible for extraction.
  const buf = await readFile(abs);
  return { content: buf.toString('base64') };
}

export async function startIndexSession(
  projectId: string,
  onProgress: ProgressListener,
): Promise<{ sessionId: string; cancel: () => void }> {
  const sessionId = randomUUID();
  const db = getDb();

  const proj = db.select().from(projects).where(eq(projects.id, projectId)).get();
  if (!proj || !proj.folderPath) {
    onProgress({ sessionId, processed: 0, total: 0, status: 'error', error: 'No folder linked' });
    // eslint-disable-next-line @typescript-eslint/no-empty-function
    return { sessionId, cancel: () => {} };
  }

  db.update(projects)
    .set({ folderLastScanStatus: 'scanning' })
    .where(eq(projects.id, projectId))
    .run();
  onProgress({ sessionId, processed: 0, total: 0, status: 'scanning' });

  const delta = await scanFolder(projectId, proj.folderPath);

  // Filter out 'skipped' files — they are too large to index and must not be sent
  const toIndex = [
    ...delta.newFiles.filter((f) => f.kind !== 'skipped'),
    ...delta.changedFiles.filter((f) => f.kind !== 'skipped'),
  ];
  const total = toIndex.length;

  for (const rel of delta.deletedRelPaths) {
    db.delete(projectFolderFiles)
      .where(
        and(
          eq(projectFolderFiles.projectId, projectId),
          eq(projectFolderFiles.relativePath, rel),
        ),
      )
      .run();
  }

  if (total === 0) {
    db.update(projects)
      .set({
        folderLastScanStatus: 'idle',
        folderLastScannedAt: Date.now(),
        folderTotalFiles: delta.unchangedCount,
      })
      .where(eq(projects.id, projectId))
      .run();
    onProgress({ sessionId, processed: 0, total: 0, status: 'completed' });
    // eslint-disable-next-line @typescript-eslint/no-empty-function
    return { sessionId, cancel: () => {} };
  }

  const backend = getBackendClient();

  let processed = 0;
  let cancelled = false;

  const finalize = (status: IndexProgress['status'], error?: string): void => {
    db.update(projects)
      .set({
        folderLastScanStatus:
          status === 'completed' || status === 'cancelled' ? 'idle' : 'error',
        folderLastScannedAt: Date.now(),
        folderTotalFiles: delta.unchangedCount + processed,
      })
      .where(eq(projects.id, projectId))
      .run();
    onProgress({ sessionId, processed, total, status, error });
  };

  backend.registerIndexSession(sessionId, {
    onFileResult: ({ relPath, summary, error }) => {
      if (error) return;
      const f = toIndex.find((x) => x.relativePath === relPath);
      if (!f) return;
      const now = Date.now();

      // SELECT-then-INSERT-or-UPDATE: no unique index on (projectId, relativePath)
      const existing = db
        .select()
        .from(projectFolderFiles)
        .where(
          and(
            eq(projectFolderFiles.projectId, projectId),
            eq(projectFolderFiles.relativePath, f.relativePath),
          ),
        )
        .get();

      if (existing) {
        db.update(projectFolderFiles)
          .set({
            mtimeMs: f.mtimeMs,
            sizeBytes: f.sizeBytes,
            kind: f.kind,
            summary: summary ?? null,
            summaryUpdatedAt: now,
          })
          .where(eq(projectFolderFiles.id, existing.id))
          .run();
      } else {
        db.insert(projectFolderFiles)
          .values({
            id: randomUUID(),
            projectId,
            relativePath: f.relativePath,
            ext: f.ext,
            kind: f.kind,
            sizeBytes: f.sizeBytes,
            mtimeMs: f.mtimeMs,
            summary: summary ?? null,
            summaryUpdatedAt: now,
          })
          .run();
      }
    },
    onProgress: ({ processed: p, total: t }) => {
      processed = p;
      onProgress({ sessionId, processed: p, total: t, status: 'scanning' });
    },
    onDone: (status) => {
      finalize(
        status === 'completed'
          ? 'completed'
          : status === 'cancelled'
            ? 'cancelled'
            : status === 'quota_exceeded'
              ? 'quota_exceeded'
              : 'error',
      );
    },
  });

  try {
    backend.sendIndexSessionStart(sessionId, projectId, total);
  } catch (err) {
    finalize('error', err instanceof Error ? err.message : 'WS send failed');
    // eslint-disable-next-line @typescript-eslint/no-empty-function
    return { sessionId, cancel: () => {} };
  }

  // Send batches (skipped files already excluded from toIndex)
  for (let i = 0; i < toIndex.length; i += INDEX_BATCH_SIZE) {
    if (cancelled) break;
    const batch = toIndex.slice(i, i + INDEX_BATCH_SIZE);
    const payload = await Promise.all(
      batch.map(async (f) => {
        const { content, mime } = await readForIndex(proj.folderPath!, f);
        return {
          relPath: f.relativePath,
          kind: f.kind as 'text' | 'image' | 'pdf' | 'docx',
          content,
          ext: f.ext,
          mime,
          sizeBytes: f.sizeBytes,
          mtimeMs: f.mtimeMs,
        };
      }),
    );
    try {
      backend.sendIndexFileBatch(sessionId, payload);
    } catch (err) {
      finalize('error', err instanceof Error ? err.message : 'WS send failed');
      // eslint-disable-next-line @typescript-eslint/no-empty-function
      return { sessionId, cancel: () => {} };
    }
  }

  const cancel = (): void => {
    cancelled = true;
    backend.sendIndexSessionCancel(sessionId);
  };
  return { sessionId, cancel };
}
