import "server-only";

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

import jpeg from "jpeg-js";

import { MediaProcessingError } from "@/lib/server/media-errors";

export interface ThumbnailFrameCandidateInput {
  id: string;
  timestampSeconds: number;
  outputPath: string;
}

export interface ThumbnailVisualMetrics {
  averageLuma: number;
  lumaStdDev: number;
  darkPixelRatio: number;
  veryDarkPixelRatio: number;
  brightPixelRatio: number;
  midtoneRatio: number;
  detailIntensity: number;
  balanceScore: number;
  contrastScore: number;
  detailScore: number;
}

export interface ScoredThumbnailCandidate extends ThumbnailFrameCandidateInput {
  score: number;
  rationale: string;
  fingerprint: string;
  metrics: ThumbnailVisualMetrics;
  rejected: boolean;
  rejectionReasons: string[];
}

interface ThumbnailScoringOptions {
  candidateCount: number;
  duplicateHashDistance?: number;
}

interface RawImageStats {
  averageLuma: number;
  lumaStdDev: number;
  darkPixelRatio: number;
  veryDarkPixelRatio: number;
  brightPixelRatio: number;
  midtoneRatio: number;
  detailIntensity: number;
  fingerprint: string;
}

function clampNumber(value: number, min: number, max: number) {
  return Math.min(max, Math.max(min, value));
}

function computeLuma(r: number, g: number, b: number) {
  return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}

function computeFingerprint(data: Uint8Array, width: number, height: number) {
  const hashGrid = 8;
  const bucketValues: number[] = [];

  for (let row = 0; row < hashGrid; row += 1) {
    const sourceY = Math.floor(((row + 0.5) / hashGrid) * height);

    for (let column = 0; column < hashGrid; column += 1) {
      const sourceX = Math.floor(((column + 0.5) / hashGrid) * width);
      const pixelX = clampNumber(sourceX, 0, Math.max(width - 1, 0));
      const pixelY = clampNumber(sourceY, 0, Math.max(height - 1, 0));
      const pixelOffset = (pixelY * width + pixelX) * 4;

      const luma = computeLuma(
        data[pixelOffset] ?? 0,
        data[pixelOffset + 1] ?? 0,
        data[pixelOffset + 2] ?? 0
      );

      bucketValues.push(luma);
    }
  }

  const average = bucketValues.reduce((sum, value) => sum + value, 0) / Math.max(bucketValues.length, 1);
  return bucketValues.map((value) => (value >= average ? "1" : "0")).join("");
}

function hammingDistance(left: string, right: string) {
  if (left.length !== right.length) {
    return Number.POSITIVE_INFINITY;
  }

  let distance = 0;
  for (let index = 0; index < left.length; index += 1) {
    if (left[index] !== right[index]) {
      distance += 1;
    }
  }

  return distance;
}

function scoreMetrics(rawStats: RawImageStats): {
  metrics: ThumbnailVisualMetrics;
  score: number;
  rejected: boolean;
  rejectionReasons: string[];
} {
  const balanceScore = clampNumber(1 - Math.abs(rawStats.averageLuma - 118) / 118, 0, 1);
  const contrastScore = clampNumber(rawStats.lumaStdDev / 68, 0, 1);
  const detailScore = clampNumber(rawStats.detailIntensity / 48, 0, 1);
  const midtoneScore = clampNumber(rawStats.midtoneRatio / 0.76, 0, 1);

  const darknessPenalty = clampNumber((rawStats.darkPixelRatio - 0.68) / 0.32, 0, 1);
  const clippingPenalty = clampNumber((rawStats.darkPixelRatio + rawStats.brightPixelRatio - 0.56) / 0.44, 0, 1);
  const lowContrastPenalty = rawStats.lumaStdDev < 20 ? clampNumber((20 - rawStats.lumaStdDev) / 20, 0, 1) : 0;

  const baseScore =
    detailScore * 0.42 +
    contrastScore * 0.23 +
    balanceScore * 0.2 +
    midtoneScore * 0.15;
  const penalty = darknessPenalty * 0.52 + clippingPenalty * 0.3 + lowContrastPenalty * 0.18;
  const score = clampNumber(baseScore - penalty, 0, 1);

  const rejectionReasons: string[] = [];

  if (rawStats.averageLuma < 20 || rawStats.veryDarkPixelRatio > 0.9 || rawStats.darkPixelRatio > 0.96) {
    rejectionReasons.push("Frame is extremely dark.");
  }

  const metrics: ThumbnailVisualMetrics = {
    averageLuma: Number(rawStats.averageLuma.toFixed(2)),
    lumaStdDev: Number(rawStats.lumaStdDev.toFixed(2)),
    darkPixelRatio: Number(rawStats.darkPixelRatio.toFixed(4)),
    veryDarkPixelRatio: Number(rawStats.veryDarkPixelRatio.toFixed(4)),
    brightPixelRatio: Number(rawStats.brightPixelRatio.toFixed(4)),
    midtoneRatio: Number(rawStats.midtoneRatio.toFixed(4)),
    detailIntensity: Number(rawStats.detailIntensity.toFixed(2)),
    balanceScore: Number(balanceScore.toFixed(4)),
    contrastScore: Number(contrastScore.toFixed(4)),
    detailScore: Number(detailScore.toFixed(4))
  };

  return {
    metrics,
    score: Number(score.toFixed(4)),
    rejected: rejectionReasons.length > 0,
    rejectionReasons
  };
}

function buildRationale(candidate: ScoredThumbnailCandidate) {
  if (candidate.rejectionReasons.length > 0) {
    return `Rejected: ${candidate.rejectionReasons.join(" ")}`;
  }

  const strengths: string[] = [];

  if (candidate.metrics.detailScore >= 0.55) {
    strengths.push("strong detail");
  }

  if (candidate.metrics.contrastScore >= 0.45) {
    strengths.push("clear contrast");
  }

  if (candidate.metrics.balanceScore >= 0.6) {
    strengths.push("balanced exposure");
  }

  if (candidate.metrics.midtoneRatio >= 0.52) {
    strengths.push("usable mid-tones");
  }

  if (strengths.length === 0) {
    return "Moderate quality frame with acceptable exposure.";
  }

  return `Selected for ${strengths.slice(0, 3).join(", ")}.`;
}

async function analyzeImage(outputPath: string): Promise<RawImageStats> {
  let imageBuffer: Buffer;

  try {
    imageBuffer = await fs.readFile(outputPath);
  } catch {
    throw new MediaProcessingError({
      message: "Failed to read generated thumbnail frame.",
      code: "filesystem_failure",
      statusCode: 500
    });
  }

  const decodedImage = jpeg.decode(imageBuffer, {
    useTArray: true
  });

  const { width, height, data } = decodedImage;
  if (!width || !height || !data || data.length === 0) {
    throw new MediaProcessingError({
      message: "Generated thumbnail frame is invalid.",
      code: "invalid_file",
      statusCode: 500
    });
  }

  const estimatedPixels = width * height;
  const stride = Math.max(1, Math.floor(Math.sqrt(estimatedPixels / 190_000)));

  let samples = 0;
  let lumaSum = 0;
  let lumaSquareSum = 0;
  let darkCount = 0;
  let veryDarkCount = 0;
  let brightCount = 0;
  let midtoneCount = 0;
  let detailSum = 0;
  let detailSamples = 0;

  for (let y = 0; y < height; y += stride) {
    for (let x = 0; x < width; x += stride) {
      const pixelOffset = (y * width + x) * 4;
      const luma = computeLuma(
        data[pixelOffset] ?? 0,
        data[pixelOffset + 1] ?? 0,
        data[pixelOffset + 2] ?? 0
      );

      lumaSum += luma;
      lumaSquareSum += luma * luma;
      samples += 1;

      if (luma < 28) {
        darkCount += 1;
      }

      if (luma < 12) {
        veryDarkCount += 1;
      }

      if (luma > 232) {
        brightCount += 1;
      }

      if (luma >= 42 && luma <= 214) {
        midtoneCount += 1;
      }

      if (x + stride < width) {
        const rightOffset = (y * width + (x + stride)) * 4;
        const rightLuma = computeLuma(
          data[rightOffset] ?? 0,
          data[rightOffset + 1] ?? 0,
          data[rightOffset + 2] ?? 0
        );

        detailSum += Math.abs(luma - rightLuma);
        detailSamples += 1;
      }

      if (y + stride < height) {
        const bottomOffset = ((y + stride) * width + x) * 4;
        const bottomLuma = computeLuma(
          data[bottomOffset] ?? 0,
          data[bottomOffset + 1] ?? 0,
          data[bottomOffset + 2] ?? 0
        );

        detailSum += Math.abs(luma - bottomLuma);
        detailSamples += 1;
      }
    }
  }

  const averageLuma = lumaSum / Math.max(samples, 1);
  const variance = lumaSquareSum / Math.max(samples, 1) - averageLuma * averageLuma;
  const lumaStdDev = Math.sqrt(Math.max(variance, 0));
  const detailIntensity = detailSum / Math.max(detailSamples, 1);

  return {
    averageLuma,
    lumaStdDev,
    darkPixelRatio: darkCount / Math.max(samples, 1),
    veryDarkPixelRatio: veryDarkCount / Math.max(samples, 1),
    brightPixelRatio: brightCount / Math.max(samples, 1),
    midtoneRatio: midtoneCount / Math.max(samples, 1),
    detailIntensity,
    fingerprint: computeFingerprint(data, width, height)
  };
}

export async function scoreThumbnailCandidates(
  candidates: ThumbnailFrameCandidateInput[],
  options: ThumbnailScoringOptions
) {
  const duplicateHashDistance = options.duplicateHashDistance ?? 9;
  const analyzedCandidates = await Promise.all(
    candidates.map(async (candidate) => {
      const rawStats = await analyzeImage(candidate.outputPath);
      const { metrics, score, rejected, rejectionReasons } = scoreMetrics(rawStats);

      const scored: ScoredThumbnailCandidate = {
        ...candidate,
        score,
        rationale: "",
        fingerprint: rawStats.fingerprint,
        metrics,
        rejected,
        rejectionReasons: [...rejectionReasons]
      };

      scored.rationale = buildRationale(scored);
      return scored;
    })
  );

  const rankedCandidates = [...analyzedCandidates].sort((left, right) => right.score - left.score);
  const selected: ScoredThumbnailCandidate[] = [];

  for (const candidate of rankedCandidates) {
    if (candidate.rejected) {
      continue;
    }

    const duplicate = selected.find((selectedCandidate) => {
      return hammingDistance(candidate.fingerprint, selectedCandidate.fingerprint) <= duplicateHashDistance;
    });

    if (duplicate) {
      candidate.rejectionReasons.push("Near-duplicate of a stronger frame.");
      candidate.rationale = `Rejected: ${candidate.rejectionReasons.join(" ")}`;
      continue;
    }

    selected.push(candidate);
    if (selected.length >= options.candidateCount) {
      break;
    }
  }

  if (selected.length < options.candidateCount) {
    for (const candidate of rankedCandidates) {
      if (selected.some((selectedCandidate) => selectedCandidate.id === candidate.id)) {
        continue;
      }

      if (candidate.rejected && candidate.rejectionReasons.includes("Frame is extremely dark.")) {
        continue;
      }

      selected.push(candidate);
      if (selected.length >= options.candidateCount) {
        break;
      }
    }
  }

  if (selected.length < options.candidateCount) {
    for (const candidate of rankedCandidates) {
      if (selected.some((selectedCandidate) => selectedCandidate.id === candidate.id)) {
        continue;
      }

      selected.push(candidate);
      if (selected.length >= options.candidateCount) {
        break;
      }
    }
  }

  const normalizedSelected = selected.slice(0, options.candidateCount).map((candidate) => {
    if (!candidate.rationale.startsWith("Rejected:")) {
      return candidate;
    }

    const hasDarknessReason = candidate.rejectionReasons.includes("Frame is extremely dark.");
    const hasDuplicateReason = candidate.rejectionReasons.includes("Near-duplicate of a stronger frame.");

    return {
      ...candidate,
      rationale: hasDarknessReason
        ? "Fallback pick from low-light footage; reroll for brighter options."
        : hasDuplicateReason
          ? "Fallback pick that is visually close to another top frame."
          : "Fallback pick to complete the candidate set."
    };
  });

  return {
    selected: normalizedSelected,
    rankedCandidates
  };
}
