import { isRemoteMediaPath } from '../jimaku/utils';
import type { MediaInput, MediaInputOptions } from '../media-input';
import type { MpvClient } from '../types/runtime';
import { extractFileUrlsFromMpvEdlSource } from './mpv-edl';

export type MediaGenerationKind = 'audio' | 'video';
export type MediaGenerationInputSource =
  | 'current-path'
  | 'stream-open-filename'
  | 'edl-stream'
  | 'youtube-cache';

export interface ResolvedMediaGenerationInput {
  path: string;
  kind: MediaGenerationKind;
  source: MediaGenerationInputSource;
  singleResolvedStream: boolean;
  inputOptions?: MediaInputOptions;
}

export interface MediaGenerationInputResolverOptions {
  getCachedMediaPath?: (
    currentVideoPath: string,
    kind: MediaGenerationKind,
  ) => Promise<string | null>;
  remoteCacheMode?: 'optional' | 'required';
  logDebug?: (message: string) => void;
}

export function resolveAudioStreamIndexForMediaGeneration(
  input: MediaInput,
  audioStreamIndex: number | null | undefined,
): number | undefined {
  if (typeof input === 'object' && 'source' in input && input.source === 'youtube-cache') {
    return undefined;
  }
  return audioStreamIndex ?? undefined;
}

const BLOCKED_HTTP_HEADER_NAMES = new Set(['authorization', 'cookie', 'proxy-authorization']);
const HTTP_HEADER_FIELD_PROPERTY_NAMES = [
  'http-header-fields',
  'options/http-header-fields',
  'file-local-options/http-header-fields',
] as const;
const USER_AGENT_PROPERTY_NAMES = [
  'file-local-options/user-agent',
  'options/user-agent',
  'user-agent',
] as const;
const REFERRER_PROPERTY_NAMES = [
  'file-local-options/referrer',
  'options/referrer',
  'referrer',
] as const;

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

function normalizeHeaderName(value: string): string | null {
  const trimmed = value.trim();
  if (!/^[A-Za-z0-9!#$%&'*+.^_`|~-]+$/.test(trimmed)) {
    return null;
  }
  if (BLOCKED_HTTP_HEADER_NAMES.has(trimmed.toLowerCase())) {
    return null;
  }
  return trimmed;
}

function extractUrlsFromMpvEdlSource(source: string): string[] {
  return extractFileUrlsFromMpvEdlSource(source)
    .map((value) => trimToNonEmptyString(value))
    .filter((value): value is string => value !== null);
}

function classifyMediaUrl(url: string): MediaGenerationKind | null {
  try {
    const mime = new URL(url).searchParams.get('mime')?.toLowerCase() ?? '';
    if (mime.startsWith('audio/')) {
      return 'audio';
    }
    if (mime.startsWith('video/')) {
      return 'video';
    }
  } catch {
    // Ignore malformed URLs and fall back to stream order.
  }

  return null;
}

function resolvePreferredUrlFromMpvEdlSource(
  source: string,
  kind: MediaGenerationKind,
): string | null {
  const urls = extractUrlsFromMpvEdlSource(source);
  if (urls.length === 0) {
    return null;
  }

  const typedMatch = urls.find((url) => classifyMediaUrl(url) === kind);
  if (typedMatch) {
    return typedMatch;
  }

  // mpv EDL sources usually list audio streams first and video streams last, so
  // when classifyMediaUrl cannot identify a typed URL we fall back to stream order.
  return kind === 'audio' ? (urls[0] ?? null) : (urls[urls.length - 1] ?? null);
}

function getHostname(value: string): string | null {
  try {
    return new URL(value).hostname.toLowerCase();
  } catch {
    return null;
  }
}

function matchesHost(hostname: string, expectedHost: string): boolean {
  return hostname === expectedHost || hostname.endsWith(`.${expectedHost}`);
}

function isGoogleVideoMediaPath(value: string): boolean {
  const host = getHostname(value);
  return Boolean(host && matchesHost(host, 'googlevideo.com'));
}

function describeMediaPathForDebugLog(value: string): string {
  try {
    const url = new URL(value);
    if (url.protocol === 'http:' || url.protocol === 'https:') {
      return `remote:${url.hostname.toLowerCase() || 'unknown'}`;
    }
    return `${url.protocol.replace(/:$/, '')}:`;
  } catch {
    // Not a URL; treat as a local file path below.
  }

  if (value.startsWith('edl://')) {
    return 'edl:';
  }

  return `local:${value}`;
}

function logMediaResolutionDebug(
  options: MediaGenerationInputResolverOptions,
  message: string,
): void {
  if (!options.logDebug) {
    return;
  }
  try {
    options.logDebug(`[media-source] ${message}`);
  } catch {
    // Debug logging should not affect media generation.
  }
}

function logResolvedMediaGenerationInput(
  options: MediaGenerationInputResolverOptions,
  currentVideoPath: string,
  result: ResolvedMediaGenerationInput,
): void {
  logMediaResolutionDebug(
    options,
    [
      `kind=${result.kind}`,
      `source=${result.source}`,
      `input=${describeMediaPathForDebugLog(result.path)}`,
      `current=${describeMediaPathForDebugLog(currentVideoPath)}`,
      `singleResolvedStream=${result.singleResolvedStream}`,
    ].join(' '),
  );
}

function logMediaGenerationInputMiss(
  options: MediaGenerationInputResolverOptions,
  kind: MediaGenerationKind,
  currentVideoPath: string,
  reason: string,
): void {
  logMediaResolutionDebug(
    options,
    [
      `kind=${kind}`,
      'source=cache-miss',
      `reason=${reason}`,
      `mode=${options.remoteCacheMode ?? 'optional'}`,
      `current=${describeMediaPathForDebugLog(currentVideoPath)}`,
    ].join(' '),
  );
}

function setHeaderIfMissing(headers: Record<string, string>, name: string, value: string): void {
  const lowerName = name.toLowerCase();
  if (!Object.keys(headers).some((existing) => existing.toLowerCase() === lowerName)) {
    headers[name] = value;
  }
}

function parseMpvHeaderField(value: string): [string, string] | null {
  const separatorIndex = value.indexOf(':');
  if (separatorIndex <= 0) {
    return null;
  }

  const name = normalizeHeaderName(value.slice(0, separatorIndex));
  const headerValue = trimToNonEmptyString(value.slice(separatorIndex + 1));
  if (!name || !headerValue) {
    return null;
  }
  return [name, headerValue.replace(/[\r\n]+/g, ' ')];
}

function toHeaderFields(value: unknown): string[] {
  if (Array.isArray(value)) {
    return value.filter((entry): entry is string => typeof entry === 'string');
  }
  if (typeof value === 'string') {
    return value
      .split(/\r?\n/)
      .map((entry) => entry.trim())
      .filter((entry) => entry.length > 0);
  }
  return [];
}

async function requestOptionalMpvProperty(
  mpvClient: Pick<MpvClient, 'requestProperty'>,
  name: string,
): Promise<unknown> {
  if (!mpvClient.requestProperty) {
    return null;
  }

  try {
    return await mpvClient.requestProperty(name);
  } catch {
    return null;
  }
}

async function requestFirstNonEmptyStringProperty(
  mpvClient: Pick<MpvClient, 'requestProperty'>,
  names: readonly string[],
): Promise<string | null> {
  for (const name of names) {
    const value = trimToNonEmptyString(await requestOptionalMpvProperty(mpvClient, name));
    if (value) {
      return value;
    }
  }
  return null;
}

async function resolveRemoteInputOptions(
  mpvClient: Pick<MpvClient, 'requestProperty'>,
  resolvedPath: string,
): Promise<MediaInputOptions | undefined> {
  if (!isRemoteMediaPath(resolvedPath) || !mpvClient.requestProperty) {
    return undefined;
  }

  const headers: Record<string, string> = {};
  for (const propertyName of HTTP_HEADER_FIELD_PROPERTY_NAMES) {
    const mpvHeaderFields = toHeaderFields(
      await requestOptionalMpvProperty(mpvClient, propertyName),
    );
    for (const field of mpvHeaderFields) {
      const parsed = parseMpvHeaderField(field);
      if (parsed) {
        headers[parsed[0]] = parsed[1];
      }
    }
  }

  const userAgent = await requestFirstNonEmptyStringProperty(mpvClient, USER_AGENT_PROPERTY_NAMES);
  const referrer = await requestFirstNonEmptyStringProperty(mpvClient, REFERRER_PROPERTY_NAMES);
  if (referrer) {
    setHeaderIfMissing(headers, 'Referer', referrer);
  }
  if (isGoogleVideoMediaPath(resolvedPath)) {
    setHeaderIfMissing(headers, 'Referer', 'https://www.youtube.com/');
    setHeaderIfMissing(headers, 'Origin', 'https://www.youtube.com');
  }

  return {
    reconnect: true,
    ...(userAgent ? { userAgent } : {}),
    ...(Object.keys(headers).length > 0 ? { headers } : {}),
  };
}

async function toResolvedMediaGenerationInput(
  mpvClient: Pick<MpvClient, 'requestProperty'>,
  path: string,
  kind: MediaGenerationKind,
  source: MediaGenerationInputSource,
  singleResolvedStream: boolean,
): Promise<ResolvedMediaGenerationInput> {
  const inputOptions = await resolveRemoteInputOptions(mpvClient, path);
  return {
    path,
    kind,
    source,
    singleResolvedStream,
    ...(inputOptions ? { inputOptions } : {}),
  };
}

export async function resolveMediaGenerationInput(
  mpvClient: Pick<MpvClient, 'currentVideoPath' | 'requestProperty'> | null | undefined,
  kind: MediaGenerationKind = 'video',
  options: MediaGenerationInputResolverOptions = {},
): Promise<ResolvedMediaGenerationInput | null> {
  const currentVideoPath = trimToNonEmptyString(mpvClient?.currentVideoPath);
  if (!currentVideoPath) {
    logMediaResolutionDebug(options, `kind=${kind} source=none reason=no-current-video`);
    return null;
  }

  if (!isRemoteMediaPath(currentVideoPath)) {
    const result: ResolvedMediaGenerationInput = {
      path: currentVideoPath,
      kind,
      source: 'current-path',
      singleResolvedStream: false,
    };
    logResolvedMediaGenerationInput(options, currentVideoPath, result);
    return result;
  }

  let cachedPath: string | null = null;
  if (options.getCachedMediaPath) {
    try {
      cachedPath = trimToNonEmptyString(await options.getCachedMediaPath(currentVideoPath, kind));
    } catch {
      cachedPath = null;
    }
  }
  if (cachedPath) {
    const result: ResolvedMediaGenerationInput = {
      path: cachedPath,
      kind,
      source: 'youtube-cache',
      singleResolvedStream: false,
    };
    logResolvedMediaGenerationInput(options, currentVideoPath, result);
    return result;
  }

  if (options.remoteCacheMode === 'required') {
    logMediaGenerationInputMiss(options, kind, currentVideoPath, 'required-cache-unavailable');
    return null;
  }

  if (!mpvClient?.requestProperty) {
    const result: ResolvedMediaGenerationInput = {
      path: currentVideoPath,
      kind,
      source: 'current-path',
      singleResolvedStream: false,
    };
    logResolvedMediaGenerationInput(options, currentVideoPath, result);
    return result;
  }

  try {
    const streamOpenFilename = trimToNonEmptyString(
      await mpvClient.requestProperty('stream-open-filename'),
    );
    if (streamOpenFilename?.startsWith('edl://')) {
      const preferredUrl = resolvePreferredUrlFromMpvEdlSource(streamOpenFilename, kind);
      if (preferredUrl) {
        const result = await toResolvedMediaGenerationInput(
          mpvClient,
          preferredUrl,
          kind,
          'edl-stream',
          true,
        );
        logResolvedMediaGenerationInput(options, currentVideoPath, result);
        return result;
      }
      const result = await toResolvedMediaGenerationInput(
        mpvClient,
        streamOpenFilename,
        kind,
        'stream-open-filename',
        false,
      );
      logResolvedMediaGenerationInput(options, currentVideoPath, result);
      return result;
    }
    if (streamOpenFilename) {
      const result = await toResolvedMediaGenerationInput(
        mpvClient,
        streamOpenFilename,
        kind,
        'stream-open-filename',
        isRemoteMediaPath(streamOpenFilename),
      );
      logResolvedMediaGenerationInput(options, currentVideoPath, result);
      return result;
    }
  } catch {
    // Fall back to the current path when mpv does not expose a resolved stream URL.
  }

  const result = await toResolvedMediaGenerationInput(
    mpvClient,
    currentVideoPath,
    kind,
    'current-path',
    false,
  );
  logResolvedMediaGenerationInput(options, currentVideoPath, result);
  return result;
}

export async function resolveMediaGenerationInputPath(
  mpvClient: Pick<MpvClient, 'currentVideoPath' | 'requestProperty'> | null | undefined,
  kind: MediaGenerationKind = 'video',
): Promise<string | null> {
  const currentVideoPath = trimToNonEmptyString(mpvClient?.currentVideoPath);
  if (!currentVideoPath) {
    return null;
  }

  if (!isRemoteMediaPath(currentVideoPath) || !mpvClient?.requestProperty) {
    return currentVideoPath;
  }

  try {
    const streamOpenFilename = trimToNonEmptyString(
      await mpvClient.requestProperty('stream-open-filename'),
    );
    if (streamOpenFilename?.startsWith('edl://')) {
      return resolvePreferredUrlFromMpvEdlSource(streamOpenFilename, kind) ?? streamOpenFilename;
    }
    if (streamOpenFilename) {
      return streamOpenFilename;
    }
  } catch {
    // Fall back to the current path when mpv does not expose a resolved stream URL.
  }

  return currentVideoPath;
}
