import { css, LitElement, type PropertyValueMap } from "lit";
import { property, state } from "lit/decorators.js";

import type { AudioSpan } from "../transcoding/types/index.ts";
import { UrlGenerator } from "../transcoding/utils/UrlGenerator.ts";
import { makeAudioBufferTask } from "./EFMedia/audioTasks/makeAudioBufferTask.ts";
import { makeAudioFrequencyAnalysisTask } from "./EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.ts";
import { makeAudioInitSegmentFetchTask } from "./EFMedia/audioTasks/makeAudioInitSegmentFetchTask.ts";
import { makeAudioInputTask } from "./EFMedia/audioTasks/makeAudioInputTask.ts";
import { makeAudioSeekTask } from "./EFMedia/audioTasks/makeAudioSeekTask.ts";
import { makeAudioSegmentFetchTask } from "./EFMedia/audioTasks/makeAudioSegmentFetchTask.ts";
import { makeAudioSegmentIdTask } from "./EFMedia/audioTasks/makeAudioSegmentIdTask.ts";
import { makeAudioTimeDomainAnalysisTask } from "./EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.ts";
import { fetchAudioSpanningTime } from "./EFMedia/shared/AudioSpanUtils.ts";
import { makeMediaEngineTask } from "./EFMedia/tasks/makeMediaEngineTask.ts";
import { EFSourceMixin } from "./EFSourceMixin.js";
import { EFTemporal } from "./EFTemporal.js";
import { FetchMixin } from "./FetchMixin.js";
import { EFTargetable } from "./TargetController.ts";
import { updateAnimations } from "./updateAnimations.ts";

// EF_FRAMEGEN is a global instance created in EF_FRAMEGEN.ts
declare global {
  var EF_FRAMEGEN: import("../EF_FRAMEGEN.js").EFFramegen;
}

const freqWeightsCache = new Map<number, Float32Array>();

export class IgnorableError extends Error {}

export const deepGetMediaElements = (
  element: Element,
  medias: EFMedia[] = [],
) => {
  for (const child of Array.from(element.children)) {
    if (child instanceof EFMedia) {
      medias.push(child);
    } else {
      deepGetMediaElements(child, medias);
    }
  }
  return medias;
};

export class EFMedia extends EFTargetable(
  EFSourceMixin(EFTemporal(FetchMixin(LitElement)), {
    assetType: "isobmff_files",
  }),
) {
  // Sample buffer size configuration
  static readonly VIDEO_SAMPLE_BUFFER_SIZE = 30;
  static readonly AUDIO_SAMPLE_BUFFER_SIZE = 120;

  static get observedAttributes() {
    // biome-ignore lint/complexity/noThisInStatic: We need to access super
    const parentAttributes = super.observedAttributes || [];
    return [
      ...parentAttributes,
      "mute",
      "fft-size",
      "fft-decay",
      "fft-gain",
      "interpolate-frequencies",
      "asset-id",
      "audio-buffer-duration",
      "max-audio-buffer-fetches",
      "enable-audio-buffering",
    ];
  }

  static styles = [
    css`
      :host {
        display: block;
        position: relative;
        overflow: hidden;
      }
    `,
  ];

  @property({ type: Number })
  currentTimeMs = 0;

  /**
   * Duration in milliseconds for audio buffering ahead of current time
   * @domAttribute "audio-buffer-duration"
   */
  @property({ type: Number, attribute: "audio-buffer-duration" })
  audioBufferDurationMs = 10000; // 10 seconds - reasonable for JIT encoding

  /**
   * Maximum number of concurrent audio segment fetches for buffering
   * @domAttribute "max-audio-buffer-fetches"
   */
  @property({ type: Number, attribute: "max-audio-buffer-fetches" })
  maxAudioBufferFetches = 2;

  /**
   * Enable/disable audio buffering system
   * @domAttribute "enable-audio-buffering"
   */
  @property({ type: Boolean, attribute: "enable-audio-buffering" })
  enableAudioBuffering = true;

  /**
   * Mute/unmute the media element
   * @domAttribute "mute"
   */
  @property({
    type: Boolean,
    attribute: "mute",
    reflect: true,
  })
  mute = false;

  /**
   * FFT size for frequency analysis
   * @domAttribute "fft-size"
   */
  @property({ type: Number, attribute: "fft-size", reflect: true })
  fftSize = 128;

  /**
   * FFT decay rate for frequency analysis
   * @domAttribute "fft-decay"
   */
  @property({ type: Number, attribute: "fft-decay", reflect: true })
  fftDecay = 8;

  /**
   * FFT gain for frequency analysis
   * @domAttribute "fft-gain"
   */
  @property({ type: Number, attribute: "fft-gain", reflect: true })
  fftGain = 3.0;

  /**
   * Enable/disable frequency interpolation
   * @domAttribute "interpolate-frequencies"
   */
  @property({
    type: Boolean,
    attribute: "interpolate-frequencies",
    reflect: true,
  })
  interpolateFrequencies = false;

  // Update FREQ_WEIGHTS to use the instance fftSize instead of a static value
  get FREQ_WEIGHTS() {
    if (freqWeightsCache.has(this.fftSize)) {
      // biome-ignore lint/style/noNonNullAssertion: We know the value is set due to the guard above
      return freqWeightsCache.get(this.fftSize)!;
    }

    const weights = new Float32Array(this.fftSize / 2).map((_, i) => {
      const frequency = (i * 48000) / this.fftSize;
      if (frequency < 60) return 0.3;
      if (frequency < 250) return 0.4;
      if (frequency < 500) return 0.6;
      if (frequency < 2000) return 0.8;
      if (frequency < 4000) return 1.2;
      if (frequency < 8000) return 1.6;
      return 2.0;
    });

    freqWeightsCache.set(this.fftSize, weights);
    return weights;
  }

  // Helper getter for backwards compatibility
  get shouldInterpolateFrequencies() {
    return this.interpolateFrequencies;
  }

  get urlGenerator() {
    return new UrlGenerator(() => this.apiHost ?? "");
  }

  mediaEngineTask = makeMediaEngineTask(this);

  audioSegmentIdTask = makeAudioSegmentIdTask(this);
  audioInitSegmentFetchTask = makeAudioInitSegmentFetchTask(this);
  audioSegmentFetchTask = makeAudioSegmentFetchTask(this);
  audioInputTask = makeAudioInputTask(this);
  audioSeekTask = makeAudioSeekTask(this);

  audioBufferTask = makeAudioBufferTask(this);

  // Audio analysis tasks for frequency and time domain analysis
  byteTimeDomainTask = makeAudioTimeDomainAnalysisTask(this);
  frequencyDataTask = makeAudioFrequencyAnalysisTask(this);

  /**
   * The unique identifier for the media asset.
   * This property can be set programmatically or via the "asset-id" attribute.
   * @domAttribute "asset-id"
   */
  @property({ type: String, attribute: "asset-id", reflect: true })
  assetId: string | null = null;

  get intrinsicDurationMs() {
    return this.mediaEngineTask.value?.durationMs ?? 0;
  }

  protected updated(
    changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>,
  ): void {
    super.updated(changedProperties);

    // Check if our timeline position has actually changed, even if ownCurrentTimeMs isn't tracked as a property
    const newCurrentSourceTimeMs = this.currentSourceTimeMs;
    if (newCurrentSourceTimeMs !== this.desiredSeekTimeMs) {
      this.executeSeek(newCurrentSourceTimeMs);
    }

    if (changedProperties.has("ownCurrentTimeMs")) {
      this.executeSeek(this.currentSourceTimeMs);
    }
    if (
      changedProperties.has("currentTime") ||
      changedProperties.has("ownCurrentTimeMs")
    ) {
      updateAnimations(this);
    }
  }

  get hasOwnDuration() {
    return true;
  }

  @state()
  private _desiredSeekTimeMs = 0; // Initialize to 0 for proper segment loading

  get desiredSeekTimeMs() {
    return this._desiredSeekTimeMs;
  }

  set desiredSeekTimeMs(value: number) {
    if (this._desiredSeekTimeMs !== value) {
      this._desiredSeekTimeMs = value;
    }
  }

  protected async executeSeek(seekToMs: number) {
    // The seekToMs parameter should be the timeline-relative media time
    // calculated from currentSourceTimeMs which includes timeline positioning
    this.desiredSeekTimeMs = seekToMs;
  }

  /**
   * Main integration method for EFTimegroup audio playback
   * Now powered by clean, testable utility functions
   */
  async fetchAudioSpanningTime(
    fromMs: number,
    toMs: number,
    signal: AbortSignal = new AbortController().signal,
  ): Promise<AudioSpan> {
    return fetchAudioSpanningTime(this, fromMs, toMs, signal);
  }
}
