import "server-only";

import { promises as fs } from "node:fs";
import path from "node:path";

import { getMediaProcessingConfig } from "@/lib/server/media-config";
import { MediaProcessingError } from "@/lib/server/media-errors";
import { getYtDlpAuthOptions } from "@/lib/social-download/yt-dlp-auth";
import { createYtDlpClient } from "@/lib/social-download/yt-dlp-client";
import { assertPathWithinRoot, createTempOutputPath } from "@/lib/uploads";
import { sanitizeBaseName } from "@/lib/validations";
import type { TikTokDownloadResult, TikTokVideoInfo } from "@/types/media-processing";

interface YoutubeDlPayload {
  title?: string;
  webpage_url?: string;
  uploader?: string;
  uploader_id?: string | null;
  uploader_url?: string | null;
  channel?: string | null;
  channel_url?: string | null;
  duration?: number | string | null;
  width?: number | string | null;
  height?: number | string | null;
  thumbnail?: string | null;
  extractor_key?: string | null;
  vcodec?: string | null;
  entries?: YoutubeDlPayload[];
  _type?: string;
  id?: string;
  formats?: YoutubeDlFormat[];
}

interface YoutubeDlFormat {
  format_id?: string;
  ext?: string;
  vcodec?: string;
  acodec?: string;
  format?: string;
  format_note?: string;
  width?: number;
  height?: number;
  tbr?: number;
  filesize?: number;
  filesize_approx?: number;
  protocol?: string;
  url?: string;
}

interface YoutubeDlError extends Error {
  stderr?: string | Buffer | null;
  stdout?: string | Buffer | null;
  shortMessage?: string | null;
  code?: string | null;
  path?: string | null;
  errno?: string | number | null;
}

function getYoutubeDlClient() {
  return createYtDlpClient();
}

function pickVideoEntry(payload: YoutubeDlPayload) {
  const entries = Array.isArray(payload.entries) ? payload.entries : [];
  if (entries.length === 0) {
    return payload;
  }

  const firstVideoEntry =
    entries.find((entry) => {
      const videoCodec = typeof entry.vcodec === "string" ? entry.vcodec : "";
      return videoCodec !== "" && videoCodec !== "none";
    }) || entries[0];

  return firstVideoEntry ?? payload;
}

function toNonEmptyString(value: unknown) {
  if (typeof value !== "string") {
    return null;
  }

  const trimmedValue = value.trim();
  return trimmedValue ? trimmedValue : null;
}

function toNonNegativeNumber(value: unknown) {
  if (typeof value === "number" && Number.isFinite(value) && value >= 0) {
    return value;
  }

  if (typeof value === "string") {
    const parsedValue = Number(value);
    if (Number.isFinite(parsedValue) && parsedValue >= 0) {
      return parsedValue;
    }
  }

  return null;
}

function extractHandleFromTikTokUrl(profileUrl: string | null) {
  if (!profileUrl) {
    return null;
  }

  try {
    const parsedUrl = new URL(profileUrl);
    const pathSegments = parsedUrl.pathname.split("/").filter(Boolean);

    for (const segment of pathSegments) {
      if (segment.startsWith("@") && segment.length > 1) {
        return segment;
      }
    }
  } catch {
    return null;
  }

  return null;
}

function toTikTokVideoInfo(url: string, payload: YoutubeDlPayload): TikTokVideoInfo {
  const selectedEntry = pickVideoEntry(payload);

  const title = toNonEmptyString(selectedEntry.title) || "TikTok Video";
  const webpageUrl = toNonEmptyString(selectedEntry.webpage_url) || url;
  const uploaderProfileUrl =
    toNonEmptyString(selectedEntry.uploader_url) || toNonEmptyString(selectedEntry.channel_url);
  const uploaderHandle =
    extractHandleFromTikTokUrl(uploaderProfileUrl) ||
    toNonEmptyString(selectedEntry.uploader) ||
    toNonEmptyString(selectedEntry.uploader_id);

  return {
    sourceUrl: url,
    webpageUrl,
    title,
    uploader: toNonEmptyString(selectedEntry.channel) || toNonEmptyString(selectedEntry.uploader),
    uploaderHandle,
    uploaderProfileUrl,
    uploaderProfileImageUrl: null,
    durationSeconds: toNonNegativeNumber(selectedEntry.duration),
    width: toNonNegativeNumber(selectedEntry.width),
    height: toNonNegativeNumber(selectedEntry.height),
    thumbnailUrl: toNonEmptyString(selectedEntry.thumbnail),
    extractorKey: toNonEmptyString(selectedEntry.extractor_key)
  };
}

function decodeTikTokEscapedString(input: string) {
  return input
    .replaceAll("\\u002F", "/")
    .replaceAll("\\u0026", "&")
    .replaceAll("\\u003D", "=")
    .replaceAll("\\u003F", "?")
    .replaceAll("\\u0025", "%")
    .replaceAll("\\u002D", "-")
    .replaceAll("\\/", "/");
}

function toSafeHttpUrl(url: string | null) {
  if (!url) {
    return null;
  }

  try {
    const parsedUrl = new URL(url);
    if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") {
      return null;
    }

    return parsedUrl.toString();
  } catch {
    return null;
  }
}

function extractProfileImageFromTikTokHtml(html: string) {
  const avatarPatterns = [
    /"avatarLarger":"([^\"]+)"/i,
    /"avatarMedium":"([^\"]+)"/i,
    /"avatarThumb":"([^\"]+)"/i
  ];

  for (const pattern of avatarPatterns) {
    const match = html.match(pattern);
    if (match?.[1]) {
      const decodedUrl = decodeTikTokEscapedString(match[1]);
      const safeUrl = toSafeHttpUrl(decodedUrl);
      if (safeUrl) {
        return safeUrl;
      }
    }
  }

  return null;
}

const TIKTOK_PROFILE_USER_AGENTS = [
  "Mozilla/5.0",
  "facebookexternalhit/1.1",
  "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36"
] as const;

async function fetchTikTokProfileImageUrl(profileUrl: string | null) {
  const safeProfileUrl = toSafeHttpUrl(profileUrl);
  if (!safeProfileUrl) {
    return null;
  }

  for (const userAgent of TIKTOK_PROFILE_USER_AGENTS) {
    try {
      const response = await fetch(safeProfileUrl, {
        headers: {
          "user-agent": userAgent,
          accept: "text/html",
          "accept-language": "en-US,en;q=0.9"
        },
        cache: "no-store"
      });

      if (!response.ok) {
        continue;
      }

      const html = await response.text();
      const profileImageUrl = extractProfileImageFromTikTokHtml(html);
      if (profileImageUrl) {
        return profileImageUrl;
      }
    } catch {
      continue;
    }
  }

  return null;
}

async function enrichTikTokInfoWithProfile(info: TikTokVideoInfo): Promise<TikTokVideoInfo> {
  const profileImageUrl = await fetchTikTokProfileImageUrl(info.uploaderProfileUrl);

  return {
    ...info,
    uploaderProfileImageUrl: profileImageUrl
  };
}

function hasVideoStream(format: YoutubeDlFormat) {
  const videoCodec = toNonEmptyString(format.vcodec);
  if (videoCodec) {
    return videoCodec.toLowerCase() !== "none";
  }

  return (toNonNegativeNumber(format.height) || 0) > 0 || (toNonNegativeNumber(format.width) || 0) > 0;
}

function hasAudioStream(format: YoutubeDlFormat) {
  const audioCodec = toNonEmptyString(format.acodec);
  if (audioCodec) {
    return audioCodec.toLowerCase() !== "none";
  }

  return false;
}

function isLikelyVideoWithAudio(format: YoutubeDlFormat) {
  if (!hasVideoStream(format)) {
    return false;
  }

  if (hasAudioStream(format)) {
    return true;
  }

  const note = toNonEmptyString(format.format_note)?.toLowerCase() || "";
  if (note.includes("video only")) {
    return false;
  }

  const audioCodec = toNonEmptyString(format.acodec);
  if (audioCodec && audioCodec.toLowerCase() === "none") {
    return false;
  }

  return true;
}

function isHttpFormat(format: YoutubeDlFormat) {
  const protocol = toNonEmptyString(format.protocol)?.toLowerCase() || "";
  return protocol.includes("https") || protocol.includes("http");
}

function scoreFormat(format: YoutubeDlFormat) {
  const formatId = toNonEmptyString(format.format_id)?.toLowerCase() || "";
  const formatLabel = toNonEmptyString(format.format)?.toLowerCase() || "";
  const formatNote = toNonEmptyString(format.format_note)?.toLowerCase() || "";
  const formatUrl = toNonEmptyString(format.url)?.toLowerCase() || "";
  const videoCodec = toNonEmptyString(format.vcodec)?.toLowerCase() || "";

  const heightScore = toNonNegativeNumber(format.height) || 0;
  const bitrateScore = toNonNegativeNumber(format.tbr) || 0;
  const sizeScore = toNonNegativeNumber(format.filesize) || toNonNegativeNumber(format.filesize_approx) || 0;

  const extBonus = format.ext === "mp4" ? 2_500_000_000_000 : 0;
  const protocolBonus = isHttpFormat(format) ? 2_000_000_000_000 : 0;
  const audioBonus = isLikelyVideoWithAudio(format) ? 1_500_000_000_000 : 0;
  const h264Bonus = videoCodec.includes("h264") || videoCodec.includes("avc") ? 350_000_000 : 0;

  const watermarkMarker = [formatId, formatLabel, formatNote, formatUrl].join(" ");
  const watermarkPenalty = watermarkMarker.includes("playwm") || watermarkMarker.includes("watermark")
    ? 800_000_000
    : 0;
  const possibleNoWatermarkBonus = formatId.endsWith("-1") ? 50_000_000 : 0;

  return (
    extBonus +
    protocolBonus +
    audioBonus +
    h264Bonus +
    possibleNoWatermarkBonus -
    watermarkPenalty +
    heightScore * 1_000_000 +
    bitrateScore * 1_000 +
    sizeScore
  );
}

function pickBestFormat(formats: YoutubeDlFormat[]) {
  if (formats.length === 0) {
    return null;
  }

  return formats.reduce((best, current) => {
    if (!best) {
      return current;
    }

    return scoreFormat(current) > scoreFormat(best) ? current : best;
  }, null as YoutubeDlFormat | null);
}

function selectPreferredDownloadFormatId(payload: YoutubeDlPayload) {
  const selectedEntry = pickVideoEntry(payload);
  const formats = Array.isArray(selectedEntry.formats) ? selectedEntry.formats : [];

  const withVideoAndAudioHttpMp4 = formats.filter(
    (format) =>
      hasVideoStream(format) &&
      isLikelyVideoWithAudio(format) &&
      isHttpFormat(format) &&
      format.ext === "mp4"
  );
  const withVideoAndAudioMp4 = formats.filter(
    (format) => hasVideoStream(format) && isLikelyVideoWithAudio(format) && format.ext === "mp4"
  );
  const withVideoAndAudioAny = formats.filter(
    (format) => hasVideoStream(format) && isLikelyVideoWithAudio(format)
  );
  const withVideoAny = formats.filter((format) => hasVideoStream(format));

  const preferredFormat =
    pickBestFormat(withVideoAndAudioHttpMp4) ||
    pickBestFormat(withVideoAndAudioMp4) ||
    pickBestFormat(withVideoAndAudioAny) ||
    pickBestFormat(withVideoAny);

  const preferredFormatId = toNonEmptyString(preferredFormat?.format_id);
  if (!preferredFormatId) {
    throw new MediaProcessingError({
      message: "No downloadable video format was found for this TikTok URL.",
      code: "invalid_input",
      statusCode: 400
    });
  }

  return preferredFormatId;
}

function toMediaProcessingError(error: unknown, fallbackMessage: string) {
  const resolvedError = error as YoutubeDlError;
  const toReadableText = (value: unknown) => {
    if (typeof value === "string") {
      const trimmedValue = value.trim();
      return trimmedValue || null;
    }

    if (value instanceof Buffer) {
      const trimmedValue = value.toString("utf8").trim();
      return trimmedValue || null;
    }

    return null;
  };

  const stderrText = toReadableText(resolvedError?.stderr) || "";
  const stderrTail = stderrText.split("\n").filter(Boolean).pop() || "";
  const shortMessage = toReadableText(resolvedError?.shortMessage) || "";
  const errorMessage = error instanceof Error ? error.message.trim() : "";
  const stringifiedError = error == null ? "" : String(error).trim();

  const readableMessage =
    stderrTail ||
    shortMessage ||
    errorMessage ||
    stringifiedError ||
    fallbackMessage;

  const lowerMessage = readableMessage.toLowerCase();
  const lowerCode = toReadableText(resolvedError?.code)?.toLowerCase() || "";

  if (lowerCode === "enoent") {
    const missingPath = toReadableText(resolvedError?.path);
    return new MediaProcessingError({
      message: missingPath
        ? `yt-dlp binary was not found at '${missingPath}'. Run npm install or set YT_DLP_PATH to a valid yt-dlp executable.`
        : "yt-dlp binary was not found. Run npm install or set YT_DLP_PATH to a valid yt-dlp executable.",
      code: "missing_binary",
      statusCode: 503
    });
  }

  const isNoVideo =
    lowerMessage.includes("video is unavailable") ||
    lowerMessage.includes("video is private") ||
    lowerMessage.includes("status code 10216") ||
    lowerMessage.includes("could not extract") ||
    lowerMessage.includes("not available");
  const isAccessIssue =
    lowerMessage.includes("private") ||
    lowerMessage.includes("login") ||
    lowerMessage.includes("sign in") ||
    lowerMessage.includes("unsupported url");

  if (isNoVideo) {
    return new MediaProcessingError({
      message: "This TikTok video is unavailable or does not expose downloadable formats.",
      code: "invalid_input",
      statusCode: 400
    });
  }

  return new MediaProcessingError({
    message:
      readableMessage && readableMessage.toLowerCase() !== "error" ? readableMessage : fallbackMessage,
    code: isAccessIssue ? "invalid_input" : "unknown",
    statusCode: isAccessIssue ? 400 : 502,
    details: stderrTail ? { stderrTail } : undefined
  });
}

async function resolveDownloadedOutputPath(initialOutputPath: string) {
  try {
    await fs.access(initialOutputPath);
    return initialOutputPath;
  } catch {
    const outputDirectory = path.dirname(initialOutputPath);
    const outputBaseName = path.basename(initialOutputPath, path.extname(initialOutputPath));
    const directoryEntries = await fs.readdir(outputDirectory);
    const candidateFiles = directoryEntries.filter((entry) => entry.startsWith(outputBaseName));

    if (candidateFiles.length === 0) {
      throw new MediaProcessingError({
        message: "Download completed but output file was not found.",
        code: "filesystem_failure",
        statusCode: 500
      });
    }

    const rankedCandidates = await Promise.all(
      candidateFiles.map(async (candidateFile) => {
        const candidatePath = path.join(outputDirectory, candidateFile);
        const candidateStats = await fs.stat(candidatePath);
        const candidateExtension = path.extname(candidateFile).toLowerCase();
        const extensionRank = candidateExtension === ".mp4" ? 3 : candidateExtension === ".webm" ? 2 : 1;

        return {
          candidatePath,
          extensionRank,
          size: candidateStats.size
        };
      })
    );

    rankedCandidates.sort((left, right) => {
      if (left.extensionRank !== right.extensionRank) {
        return right.extensionRank - left.extensionRank;
      }

      return right.size - left.size;
    });

    return rankedCandidates[0].candidatePath;
  }
}

async function fetchTikTokPayload(url: string) {
  const ytDlpClient = getYoutubeDlClient();
  const config = getMediaProcessingConfig();
  const authOptions = getYtDlpAuthOptions("tiktok");

  return (await ytDlpClient(
    url,
    {
      skipDownload: true,
      dumpSingleJson: true,
      noWarnings: true,
      noPlaylist: true,
      noCheckCertificates: true,
      ...authOptions
    },
    {
      timeout: config.maxCommandDurationMs
    }
  )) as YoutubeDlPayload;
}

export async function fetchTikTokVideoInfo(url: string): Promise<TikTokVideoInfo> {
  try {
    const payload = await fetchTikTokPayload(url);
    const baseInfo = toTikTokVideoInfo(url, payload);
    return enrichTikTokInfoWithProfile(baseInfo);
  } catch (error) {
    throw toMediaProcessingError(error, "Failed to fetch TikTok media info.");
  }
}

export async function downloadTikTokVideo(url: string): Promise<TikTokDownloadResult> {
  const ytDlpClient = getYoutubeDlClient();
  const config = getMediaProcessingConfig();
  const authOptions = getYtDlpAuthOptions("tiktok");

  const payload = await fetchTikTokPayload(url);
  const info = await enrichTikTokInfoWithProfile(toTikTokVideoInfo(url, payload));
  const selectedFormatId = selectPreferredDownloadFormatId(payload);
  const initialOutputPath = await createTempOutputPath({
    extension: "mp4",
    prefix: "tiktok-video"
  });

  try {
    await ytDlpClient(
      url,
      {
        format: selectedFormatId,
        output: initialOutputPath,
        noWarnings: true,
        noPlaylist: true,
        noCheckCertificates: true,
        forceOverwrites: true,
        ...authOptions
      },
      {
        timeout: config.maxCommandDurationMs
      }
    );

    const resolvedOutputPath = await resolveDownloadedOutputPath(initialOutputPath);
    assertPathWithinRoot(resolvedOutputPath, config.tempRootDirectory);

    const safeBaseName = sanitizeBaseName(`${info.title}-${Date.now().toString(36)}`);
    const fileName = `${safeBaseName}.mp4`;

    return {
      outputPath: resolvedOutputPath,
      fileName,
      info
    };
  } catch (error) {
    throw toMediaProcessingError(error, "Failed to download TikTok video.");
  }
}
