import { KtxIngestEmbeddingPortAdapter } from './context/llm/embedding-port.js';
import type { KtxEmbeddingPort } from './context/core/embedding.js';
import { reindexLocalIndexes } from './context/index-sync/reindex.js';
import type { ReindexScopeResult, ReindexSummary } from './context/index-sync/types.js';
import { loadKtxProject } from './context/project/project.js';
import { Option, type Command } from '@commander-js/extra-typings';
import { cancel, intro, log, note, outro } from '@clack/prompts';
import type { KtxCliCommandContext } from './cli-program.js';
import type { KtxCliIo } from './cli-runtime.js';
import { resolveProjectEmbeddingProvider } from './embedding-resolution.js';
import { resolveOutputMode } from './io/mode.js';
import { green, red, SYMBOLS } from './io/symbols.js';

export interface KtxAdminReindexArgs {
  projectDir: string;
  force: boolean;
  output?: 'pretty' | 'plain' | 'json';
  json?: boolean;
  cliVersion: string;
}

export function registerAdminReindexCommand(admin: Command, context: KtxCliCommandContext): void {
  admin
    .command('reindex')
    .description('Sync local wiki and semantic-layer search indexes from disk')
    .option('--force', 'Clear each discovered scope before rebuilding it', false)
    .option('--json', 'Shortcut for --output=json (overrides --output)', false)
    .addOption(
      new Option('--output <mode>', 'Output mode: pretty, plain, or json').choices(['pretty', 'plain', 'json']),
    )
    .action(async (options: { force?: boolean; json?: boolean; output?: 'pretty' | 'plain' | 'json' }, command) => {
      const runner = context.deps.adminReindex ?? runKtxAdminReindex;
      const { resolveCommandProjectDir } = await import('./cli-program.js');
      context.setExitCode(
        await runner(
          {
            projectDir: resolveCommandProjectDir(command),
            force: options.force === true,
            json: options.json === true,
            output: options.output,
            cliVersion: context.packageInfo.version,
          },
          context.io,
        ),
      );
    });
}

function scopeKey(scope: ReindexScopeResult): string {
  if (scope.kind === 'wiki') {
    return scope.scope === 'user' ? `wiki/user/${scope.scopeId ?? 'local'}` : 'wiki/global';
  }
  return `sl/${scope.connectionId ?? scope.label}`;
}

function quotePlainValue(value: string): string {
  return `"${value.replaceAll('\\', '\\\\').replaceAll('"', '\\"')}"`;
}

/** @internal */
export function reindexHasErrors(summary: ReindexSummary): boolean {
  return summary.scopes.some((scope) => scope.error);
}

/** @internal */
export function renderReindexPlain(summary: ReindexSummary, io: KtxCliIo): void {
  const updateKey = summary.force ? 'rebuilt' : 'updated';
  for (const scope of summary.scopes) {
    const cells = [
      scopeKey(scope),
      `scanned=${scope.scanned}`,
      `${updateKey}=${scope.updated}`,
      `deleted=${scope.deleted}`,
      `embeddings=${summary.embeddingsAvailable ? String(scope.embeddingsRecomputed) : '-'}`,
      `duration_ms=${scope.durationMs}`,
      ...(scope.error ? [`error=${quotePlainValue(scope.error)}`] : []),
    ];
    io.stderr.write(`${cells.join('\t')}\n`);
  }
  const failed = summary.scopes.filter((scope) => scope.error).length;
  io.stdout.write(
    [
      'reindex',
      `scopes=${summary.scopes.length}`,
      `scanned=${summary.totals.scanned}`,
      `${updateKey}=${summary.totals.updated}`,
      `deleted=${summary.totals.deleted}`,
      `embeddings=${summary.embeddingsAvailable ? String(summary.totals.embeddingsRecomputed) : '-'}`,
      `duration_ms=${summary.durationMs}`,
      ...(failed > 0 ? [`failed=${failed}`] : []),
    ].join('\t') + '\n',
  );
}

/** @internal */
export function renderReindexJson(summary: ReindexSummary, io: KtxCliIo): void {
  io.stdout.write(`${JSON.stringify({ kind: 'reindex', data: summary, meta: { command: 'admin reindex' } }, null, 2)}\n`);
}

function noun(scope: ReindexScopeResult): string {
  return scope.kind === 'wiki' ? 'pages' : 'sources';
}

function formatScopeLine(scope: ReindexScopeResult, force: boolean, embeddingsAvailable: boolean): string {
  if (scope.error) {
    return `${scope.kind === 'wiki' ? 'Wiki' : 'SL'}: ${scope.label} ${SYMBOLS.emDash} failed: ${scope.error}`;
  }
  const changedLabel = force ? 'rebuilt' : 'updated';
  const parts = [`${scope.scanned} ${noun(scope)}`];
  if (scope.updated > 0) {
    parts.push(`${scope.updated} ${changedLabel}`);
  } else {
    parts.push('unchanged');
  }
  if (!force && scope.deleted > 0) {
    parts.push(`${scope.deleted} deleted`);
  }
  if (embeddingsAvailable) {
    parts.push(`${scope.embeddingsRecomputed} embeddings recomputed`);
  }
  parts.push(`${scope.durationMs}ms`);
  return `${scope.kind === 'wiki' ? 'Wiki' : 'SL'}: ${scope.label} ${SYMBOLS.emDash} ${parts.join(` ${SYMBOLS.middot} `)}`;
}

function renderReindexPretty(summary: ReindexSummary, io: KtxCliIo): void {
  intro(summary.force ? 'ktx admin reindex --force' : 'ktx admin reindex');
  if (!summary.embeddingsAvailable) {
    log.warn(`Embeddings: not configured ${SYMBOLS.emDash} indexing lexical only`);
  }
  for (const scope of summary.scopes) {
    const line = formatScopeLine(scope, summary.force, summary.embeddingsAvailable);
    if (scope.error) {
      log.error(red(line));
    } else {
      log.success(green(line));
    }
  }
  const failed = summary.scopes.filter((scope) => scope.error).length;
  note(
    [
      `scopes        ${summary.scopes.length}`,
      `scanned       ${summary.totals.scanned}`,
      `${summary.force ? 'rebuilt' : 'updated'}       ${summary.totals.updated}`,
      `deleted       ${summary.totals.deleted}`,
      `embeddings    ${summary.embeddingsAvailable ? summary.totals.embeddingsRecomputed : SYMBOLS.emDash}`,
      `index         ${summary.dbPath}`,
      ...(failed > 0 ? [`failed        ${failed}`] : []),
    ].join('\n'),
    'Summary',
  );
  if (failed > 0) {
    cancel(`reindex completed with ${failed} error${failed === 1 ? '' : 's'}`);
  } else {
    outro(`Done in ${(summary.durationMs / 1000).toFixed(1)}s`);
  }
  void io;
}

async function runKtxAdminReindex(args: KtxAdminReindexArgs, io: KtxCliIo = process): Promise<number> {
  try {
    const project = await loadKtxProject({ projectDir: args.projectDir });
    const resolution = await resolveProjectEmbeddingProvider(project, {
      mode: 'use-if-running',
      cliVersion: args.cliVersion,
      io,
    });
    const embeddingService: KtxEmbeddingPort | null =
      resolution.kind === 'configured' || resolution.kind === 'managed-running' || resolution.kind === 'managed-started'
        ? new KtxIngestEmbeddingPortAdapter(resolution.provider)
        : null;
    const summary = await reindexLocalIndexes(project, { force: args.force, embeddingService });
    const mode = resolveOutputMode({ explicit: args.output, json: args.json, io });

    if (!summary.embeddingsAvailable && mode === 'plain') {
      io.stderr.write(`Embeddings: not configured ${SYMBOLS.emDash} indexing lexical only\n`);
    }

    if (mode === 'json') {
      renderReindexJson(summary, io);
    } else if (mode === 'plain') {
      renderReindexPlain(summary, io);
    } else {
      renderReindexPretty(summary, io);
    }
    return reindexHasErrors(summary) ? 1 : 0;
  } catch (error) {
    io.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
    return 1;
  }
}
