import type { MediaGenerator } from '../../../media-generator.js';
import type { AnkiConnectConfig } from '../../../types.js';
import { applyCardKindFlagFields } from '../../../anki-integration/card-kinds.js';
import { resolveWordCardKind } from '../../../anki-integration/note-field-utils.js';
import { createLogger } from '../../../logger.js';
import type { RetimedSecondarySubtitleInput } from '../secondary-subtitle-sidecar.js';

export type StatsServerNoteInfo = {
  noteId: number;
  fields: Record<string, { value: string }>;
};

export type StatsServerMediaGenerator = {
  generateAudio: (...args: Parameters<MediaGenerator['generateAudio']>) => Promise<Buffer | null>;
  generateScreenshot: (
    ...args: Parameters<MediaGenerator['generateScreenshot']>
  ) => Promise<Buffer | null>;
  generateAnimatedImage: (
    ...args: Parameters<MediaGenerator['generateAnimatedImage']>
  ) => Promise<Buffer | null>;
};

export type StatsMiningTimingEvent = {
  mode: 'word' | 'sentence' | 'audio';
  phase: string;
  elapsedMs: number;
  noteId?: number;
};

export type StatsMiningRouteOptions = {
  ankiConnectConfig?: AnkiConnectConfig;
  getAnkiConnectConfig?: () => AnkiConnectConfig | undefined;
  getYomitanAnkiDeckName?: () => Promise<string | null | undefined> | string | null | undefined;
  secondarySubtitleLanguages?: string[];
  getSecondarySubtitleLanguages?: () => string[] | undefined;
  statsMiningAlassPath?: string;
  getStatsMiningAlassPath?: () => string | null | undefined;
  resolveRetimedSecondarySubtitleText?: (
    input: RetimedSecondarySubtitleInput,
  ) => Promise<string> | string;
  addYomitanNote?: (word: string) => Promise<number | null>;
  createMediaGenerator?: () => StatsServerMediaGenerator;
  onMiningTiming?: (event: StatsMiningTimingEvent) => void;
  nowMs?: () => number;
};

type StatsMiningLogger = {
  debug: (message: string, ...meta: unknown[]) => void;
  warn: (message: string, ...meta: unknown[]) => void;
};

export const statsMiningLogger: StatsMiningLogger = createLogger('stats:mining');

export function resolveStatsNoteFieldName(
  noteInfo: StatsServerNoteInfo,
  ...preferredNames: (string | undefined)[]
): string | null {
  for (const preferredName of preferredNames) {
    if (!preferredName) continue;
    const resolved = Object.keys(noteInfo.fields).find(
      (fieldName) => fieldName.toLowerCase() === preferredName.toLowerCase(),
    );
    if (resolved) return resolved;
  }
  return null;
}

function uniqueFieldNames(...fieldNames: (string | null | undefined)[]): string[] {
  const seen = new Set<string>();
  const result: string[] = [];
  for (const fieldName of fieldNames) {
    const normalized = fieldName?.trim();
    if (!normalized) continue;
    const key = normalized.toLowerCase();
    if (seen.has(key)) continue;
    seen.add(key);
    result.push(normalized);
  }
  return result;
}

export function getStatsWordMiningAudioFieldName(
  ankiConfig: AnkiConnectConfig,
  noteInfo: StatsServerNoteInfo | null,
): string {
  return (
    (noteInfo
      ? resolveStatsNoteFieldName(noteInfo, 'SentenceAudio', ankiConfig.fields?.audio)
      : null) ??
    ankiConfig.fields?.audio ??
    'ExpressionAudio'
  );
}

export function shouldUseStatsLapisKikuCardFields(ankiConfig: AnkiConnectConfig): boolean {
  return ankiConfig.isLapis?.enabled === true || ankiConfig.isKiku?.enabled === true;
}

export function applyStatsWordCardFields(
  fields: Record<string, string>,
  noteInfo: StatsServerNoteInfo | null,
  ankiConfig: AnkiConnectConfig,
): void {
  if (!noteInfo) return;
  const cardKind = resolveWordCardKind(noteInfo, {
    lapisEnabled: ankiConfig.isLapis?.enabled === true,
    kikuEnabled: ankiConfig.isKiku?.enabled === true,
    wordCardKind: ankiConfig.lapisKiku?.wordCardKind,
  });
  if (!cardKind) return;

  applyCardKindFlagFields(fields, cardKind, (preferredName) =>
    resolveStatsNoteFieldName(noteInfo, preferredName),
  );
}

export function getStatsDirectMiningAudioFieldNames(
  ankiConfig: AnkiConnectConfig,
  noteInfo: StatsServerNoteInfo | null,
  mode: 'sentence' | 'audio',
): string[] {
  const configuredAudioField = ankiConfig.fields?.audio ?? 'ExpressionAudio';
  if (!ankiConfig.isLapis?.enabled && !ankiConfig.isKiku?.enabled) {
    return [configuredAudioField];
  }
  const sentenceAudioField = noteInfo
    ? resolveStatsNoteFieldName(noteInfo, 'SentenceAudio', configuredAudioField)
    : 'SentenceAudio';
  const expressionAudioField = noteInfo
    ? resolveStatsNoteFieldName(noteInfo, configuredAudioField)
    : configuredAudioField;
  return mode === 'sentence'
    ? uniqueFieldNames(sentenceAudioField)
    : uniqueFieldNames(sentenceAudioField, expressionAudioField);
}

export function createStatsMiningContext(options?: StatsMiningRouteOptions) {
  const nowMs = options?.nowMs ?? (() => Date.now());
  const getAnkiConnectConfig = (): AnkiConnectConfig | undefined =>
    options?.getAnkiConnectConfig?.() ?? options?.ankiConnectConfig;
  const getSecondarySubtitleLanguages = (): string[] =>
    options?.getSecondarySubtitleLanguages?.() ?? options?.secondarySubtitleLanguages ?? [];
  const getStatsMiningAlassPath = (): string | null | undefined =>
    options?.getStatsMiningAlassPath?.() ?? options?.statsMiningAlassPath;
  const getEffectiveMiningDeckName = async (ankiConfig: AnkiConnectConfig): Promise<string> => {
    const configuredDeckName = ankiConfig.deck?.trim() ?? '';
    if (configuredDeckName) return configuredDeckName;
    try {
      const yomitanDeckName = await options?.getYomitanAnkiDeckName?.();
      return typeof yomitanDeckName === 'string' ? yomitanDeckName.trim() : '';
    } catch (error) {
      statsMiningLogger.warn(
        'Failed to resolve Yomitan Anki deck for stats mining:',
        error instanceof Error ? error.message : String(error),
      );
      return '';
    }
  };
  const recordMiningTiming = (event: StatsMiningTimingEvent): void => {
    try {
      options?.onMiningTiming?.(event);
    } catch {
      // Timing observers must not affect mining execution.
    }
    statsMiningLogger.debug(
      `[stats:mining] ${event.mode} ${event.phase} ${Math.round(event.elapsedMs)}ms`,
      event,
    );
  };
  const timeMiningPhase = async <T>(
    mode: StatsMiningTimingEvent['mode'],
    phase: string,
    fn: () => Promise<T>,
    details?: (value: T) => Partial<StatsMiningTimingEvent>,
  ): Promise<T> => {
    const startedAtMs = nowMs();
    try {
      const value = await fn();
      recordMiningTiming({
        mode,
        phase,
        elapsedMs: nowMs() - startedAtMs,
        ...details?.(value),
      });
      return value;
    } catch (error) {
      recordMiningTiming({ mode, phase, elapsedMs: nowMs() - startedAtMs });
      throw error;
    }
  };

  return {
    getAnkiConnectConfig,
    getEffectiveMiningDeckName,
    getSecondarySubtitleLanguages,
    getStatsMiningAlassPath,
    timeMiningPhase,
  };
}
