import { DEFAULT_ANKI_CONNECT_CONFIG } from '../config';
import type { MediaInput } from '../media-input';
import type { MediaGenerator } from '../media-generator';
import type { AnkiConnectConfig } from '../types/anki';
import type { SubtitleMiningContext } from '../types/subtitle';
import { youtubeMediaUrlsMatch, type PendingYoutubeMediaUpdate } from './pending-youtube-media';
import type { MediaGenerationInputResolverOptions } from './media-source';

type PendingYoutubeMediaUpdateResult = 'updated' | 'partial' | 'failed';

export interface PendingYoutubeMediaQueueReadyOptions {
  notifyNoQueued?: boolean;
}

export interface PendingYoutubeMediaQueueFailedOptions {
  notifyStatus?: boolean;
}

export interface PendingYoutubeMediaNoteInfo {
  noteId: number;
  fields: Record<string, { value: string }>;
}

export interface PendingYoutubeMediaQueueDeps {
  client: {
    notesInfo(noteIds: number[]): Promise<unknown>;
    updateNoteFields(noteId: number, fields: Record<string, string>): Promise<void>;
    storeMediaFile(filename: string, data: Buffer): Promise<void>;
  };
  mediaGenerator: Pick<
    MediaGenerator,
    'generateAudio' | 'generateScreenshot' | 'generateAnimatedImage'
  >;
  getConfig: () => AnkiConnectConfig;
  getCurrentVideoPath: () => Promise<string | undefined> | string | undefined;
  getCachedMediaPath: MediaGenerationInputResolverOptions['getCachedMediaPath'] | null;
  shouldRequireRemoteMediaCache: () => boolean;
  getSubtitleMediaRange: (context?: SubtitleMiningContext) => {
    startTime: number;
    endTime: number;
  };
  resolveConfiguredFieldName: (
    noteInfo: PendingYoutubeMediaNoteInfo,
    ...preferredNames: (string | undefined)[]
  ) => string | null;
  mergeFieldValue: (existing: string, newValue: string, overwrite: boolean) => string;
  getAnimatedImageLeadInSeconds: (noteInfo: PendingYoutubeMediaNoteInfo) => Promise<number>;
  getMpvVolumeScale: () => Promise<number | undefined>;
  generateAudioFilename: () => string;
  generateImageFilename: () => string;
  formatMiscInfoPatternForMediaPath: (
    fallbackFilename: string,
    startTimeSeconds: number | undefined,
    mediaPath: string,
    mediaTitle?: string,
  ) => string;
  showStatusNotification: (message: string) => void;
  showNotification: (noteId: number, label: string | number, errorSuffix?: string) => Promise<void>;
  logInfo: (message: string, ...args: unknown[]) => void;
  logWarn: (message: string, ...args: unknown[]) => void;
  logError: (message: string, ...args: unknown[]) => void;
}

function trimToNonEmptyString(value: unknown): string | null {
  if (typeof value !== 'string') {
    return null;
  }
  const trimmed = value.trim();
  return trimmed.length > 0 ? trimmed : null;
}

function shouldGenerateAudio(config: AnkiConnectConfig): boolean {
  return config.media?.generateAudio !== false;
}

function shouldGenerateImage(config: AnkiConnectConfig): boolean {
  return config.media?.generateImage !== false;
}

export class PendingYoutubeMediaQueue {
  private updates: PendingYoutubeMediaUpdate[] = [];

  constructor(private readonly deps: PendingYoutubeMediaQueueDeps) {}

  enqueue(job: PendingYoutubeMediaUpdate): void {
    if (!job.generateAudio && !job.generateImage) {
      return;
    }
    const isFirstQueuedForSource = !this.updates.some((existing) =>
      youtubeMediaUrlsMatch(existing.sourceUrl, job.sourceUrl),
    );
    this.updates.push(job);
    this.deps.logInfo('Queued YouTube media update for note:', job.noteId);
    if (isFirstQueuedForSource) {
      this.deps.showStatusNotification(
        'YouTube media cache is still downloading. Card media will be added when the cache is ready.',
      );
    }
  }

  async queueFromNote(job: {
    noteId: number;
    noteInfo: PendingYoutubeMediaNoteInfo;
    context?: SubtitleMiningContext;
    label: string | number;
  }): Promise<boolean> {
    const sourceUrl = trimToNonEmptyString(await this.deps.getCurrentVideoPath());
    const getCachedMediaPath = this.deps.getCachedMediaPath;
    if (!sourceUrl || this.deps.shouldRequireRemoteMediaCache() !== true || !getCachedMediaPath) {
      return false;
    }

    let cachedPath: string | null = null;
    try {
      cachedPath = await getCachedMediaPath(sourceUrl, 'video');
    } catch (error) {
      this.deps.logWarn(
        'Failed to read YouTube cache state; falling back to immediate media generation:',
        error instanceof Error ? error.message : String(error),
      );
      return false;
    }
    if (cachedPath) {
      return false;
    }

    const config = this.deps.getConfig();
    const mediaRange = this.deps.getSubtitleMediaRange(job.context);
    const volumeScale = shouldGenerateAudio(config)
      ? await this.deps.getMpvVolumeScale()
      : undefined;
    this.enqueue({
      sourceUrl,
      noteId: job.noteId,
      startTime: mediaRange.startTime,
      endTime: mediaRange.endTime,
      label: job.label,
      audioFieldName: this.resolveConfiguredAudioFieldName(job.noteInfo) ?? undefined,
      imageFieldName:
        this.deps.resolveConfiguredFieldName(
          job.noteInfo,
          config.fields?.image,
          DEFAULT_ANKI_CONNECT_CONFIG.fields.image,
        ) ?? undefined,
      miscInfoFieldName:
        this.deps.resolveConfiguredFieldName(job.noteInfo, config.fields?.miscInfo) ?? undefined,
      generateAudio: shouldGenerateAudio(config),
      generateImage: shouldGenerateImage(config),
      volumeScale,
      ...(job.context?.mediaPaddingSeconds !== undefined
        ? { mediaPaddingSeconds: job.context.mediaPaddingSeconds }
        : {}),
    });
    return true;
  }

  async handleReady(
    sourceUrl: string,
    cachedPath: string,
    options: PendingYoutubeMediaQueueReadyOptions = {},
  ): Promise<void> {
    const jobs = this.takeMatchingUpdates(sourceUrl);
    if (jobs.length === 0) {
      if (options.notifyNoQueued !== false) {
        this.deps.showStatusNotification('YouTube media cache ready.');
      }
      return;
    }

    this.deps.showStatusNotification(
      `YouTube media cache ready. Adding media to ${jobs.length} queued card${
        jobs.length === 1 ? '' : 's'
      }.`,
    );

    let updatedCount = 0;
    let partialCount = 0;
    let failedCount = 0;
    for (const job of jobs) {
      try {
        const result = await this.applyUpdate(job, cachedPath);
        if (result === 'updated') {
          updatedCount += 1;
        } else if (result === 'partial') {
          partialCount += 1;
        } else {
          failedCount += 1;
        }
      } catch (error) {
        failedCount += 1;
        this.deps.logError(
          'Failed to apply queued YouTube media update:',
          error instanceof Error ? error.message : String(error),
        );
      }
    }

    if (partialCount > 0 || failedCount > 0) {
      this.deps.showStatusNotification(
        `Queued YouTube media finished with ${updatedCount} updated, ${partialCount} partial, and ${failedCount} failed.`,
      );
    }
  }

  async handleFailed(
    sourceUrl: string,
    options: PendingYoutubeMediaQueueFailedOptions = {},
  ): Promise<void> {
    const jobs = this.takeMatchingUpdates(sourceUrl);
    if (jobs.length === 0) {
      if (options.notifyStatus !== false) {
        this.deps.showStatusNotification('YouTube media cache failed.');
      }
      return;
    }

    if (options.notifyStatus !== false) {
      this.deps.showStatusNotification(
        `YouTube media cache failed. Media was not added to ${jobs.length} queued card${
          jobs.length === 1 ? '' : 's'
        }.`,
      );
    }
    this.deps.logWarn('Discarding queued YouTube media updates after cache failure:', jobs.length);

    for (const job of jobs) {
      try {
        await this.deps.showNotification(job.noteId, job.label, 'media cache failed');
      } catch (error) {
        this.deps.logWarn(
          'Failed to show queued YouTube media failure notification:',
          error instanceof Error ? error.message : String(error),
        );
      }
    }
  }

  private takeMatchingUpdates(sourceUrl: string): PendingYoutubeMediaUpdate[] {
    const matched: PendingYoutubeMediaUpdate[] = [];
    const remaining: PendingYoutubeMediaUpdate[] = [];
    for (const job of this.updates) {
      if (youtubeMediaUrlsMatch(job.sourceUrl, sourceUrl)) {
        matched.push(job);
      } else {
        remaining.push(job);
      }
    }
    this.updates = remaining;
    return matched;
  }

  private resolveConfiguredAudioFieldName(noteInfo: PendingYoutubeMediaNoteInfo): string | null {
    const config = this.deps.getConfig();
    return this.deps.resolveConfiguredFieldName(
      noteInfo,
      config.fields?.audio ?? DEFAULT_ANKI_CONNECT_CONFIG.fields.audio,
    );
  }

  private async applyUpdate(
    job: PendingYoutubeMediaUpdate,
    cachedPath: string,
  ): Promise<PendingYoutubeMediaUpdateResult> {
    const notesInfoResult = await this.deps.client.notesInfo([job.noteId]);
    const notesInfo = notesInfoResult as unknown as PendingYoutubeMediaNoteInfo[];
    const noteInfo = notesInfo[0];
    if (!noteInfo) {
      this.deps.logWarn('Queued YouTube media target note not found:', job.noteId);
      return 'failed';
    }

    const config = this.deps.getConfig();
    const mediaFields: Record<string, string> = {};
    const errors: string[] = [];
    let miscInfoFilename: string | null = null;
    const cachedMediaInput: MediaInput = {
      path: cachedPath,
      source: 'youtube-cache',
    };

    if (job.generateAudio) {
      try {
        const audioFilename = this.deps.generateAudioFilename();
        const audioBuffer = await this.deps.mediaGenerator.generateAudio(
          cachedMediaInput,
          job.startTime,
          job.endTime,
          job.mediaPaddingSeconds ?? config.media?.audioPadding,
          undefined,
          config.media?.normalizeAudio !== false,
          job.volumeScale,
        );
        if (audioBuffer) {
          await this.deps.client.storeMediaFile(audioFilename, audioBuffer);
          const audioField =
            job.audioFieldName || this.resolveConfiguredAudioFieldName(noteInfo) || null;
          if (audioField) {
            const existingAudio = noteInfo.fields[audioField]?.value || '';
            mediaFields[audioField] = this.deps.mergeFieldValue(
              existingAudio,
              `[sound:${audioFilename}]`,
              config.behavior?.overwriteAudio !== false,
            );
          }
          miscInfoFilename = audioFilename;
        }
      } catch (error) {
        errors.push('audio');
        this.deps.logError('Failed to generate queued YouTube audio:', (error as Error).message);
      }
    }

    if (job.generateImage) {
      try {
        const imageFilename = this.deps.generateImageFilename();
        const animatedLeadInSeconds = await this.deps.getAnimatedImageLeadInSeconds(noteInfo);
        const imageBuffer = await this.generateImageFromInput(
          cachedMediaInput,
          job.startTime,
          job.endTime,
          animatedLeadInSeconds,
          job.mediaPaddingSeconds,
        );
        if (imageBuffer) {
          await this.deps.client.storeMediaFile(imageFilename, imageBuffer);
          const imageField =
            job.imageFieldName ||
            this.deps.resolveConfiguredFieldName(
              noteInfo,
              config.fields?.image,
              DEFAULT_ANKI_CONNECT_CONFIG.fields.image,
            );
          if (imageField) {
            const existingImage = noteInfo.fields[imageField]?.value || '';
            mediaFields[imageField] = this.deps.mergeFieldValue(
              existingImage,
              `<img src="${imageFilename}">`,
              config.behavior?.overwriteImage !== false,
            );
          } else {
            this.deps.logWarn(
              'Image field not found on queued YouTube media note, skipping image update',
            );
          }
          miscInfoFilename = imageFilename;
        }
      } catch (error) {
        errors.push('image');
        this.deps.logError('Failed to generate queued YouTube image:', (error as Error).message);
      }
    }

    if (config.fields?.miscInfo && miscInfoFilename) {
      const miscInfoField =
        job.miscInfoFieldName ||
        this.deps.resolveConfiguredFieldName(noteInfo, config.fields.miscInfo);
      const miscInfo = this.deps.formatMiscInfoPatternForMediaPath(
        miscInfoFilename,
        job.startTime,
        job.sourceUrl,
      );
      if (miscInfoField && miscInfo) {
        mediaFields[miscInfoField] = miscInfo;
      }
    }

    if (Object.keys(mediaFields).length === 0) {
      return 'failed';
    }

    await this.deps.client.updateNoteFields(job.noteId, mediaFields);
    const errorSuffix = errors.length > 0 ? `${errors.join(', ')} failed` : undefined;
    await this.deps.showNotification(job.noteId, job.label, errorSuffix);
    this.deps.logInfo('Applied queued YouTube media update for note:', job.noteId);
    return errors.length === 0 ? 'updated' : 'partial';
  }

  private async generateImageFromInput(
    videoPath: MediaInput,
    startTime: number,
    endTime: number,
    animatedLeadInSeconds = 0,
    mediaPaddingSeconds?: number,
  ): Promise<Buffer | null> {
    const config = this.deps.getConfig();
    if (config.media?.imageType === 'avif') {
      return this.deps.mediaGenerator.generateAnimatedImage(
        videoPath,
        startTime,
        endTime,
        mediaPaddingSeconds ?? config.media?.audioPadding,
        {
          fps: config.media?.animatedFps,
          maxWidth: config.media?.animatedMaxWidth,
          maxHeight: config.media?.animatedMaxHeight,
          crf: config.media?.animatedCrf,
          leadingStillDuration: animatedLeadInSeconds,
        },
      );
    }

    const timestamp = startTime + (endTime - startTime) / 2;
    return this.deps.mediaGenerator.generateScreenshot(videoPath, timestamp, {
      format: config.media?.imageFormat as 'jpg' | 'png' | 'webp',
      quality: config.media?.imageQuality,
      maxWidth: config.media?.imageMaxWidth,
      maxHeight: config.media?.imageMaxHeight,
    });
  }
}
