import 'server-only';
import type { GameFacts, GeneratedContent, QualityGateResult } from './types';

// This is the layer that actually keeps the site out of Google's "scaled
// content abuse" bucket: nothing reaches ContentStatus.PUBLISHED without
// passing here. Three independent checks, any of which can fail the gate:
//   1. Source data completeness — is there enough real fact to justify a
//      page at all yet? (maps to "expand pages as more data arrives")
//   2. Generated text quality — long enough, and free of generic filler.
//   3. Near-duplicate detection — catches templated pages that only swap
//      the game name, which is explicitly called out in Google's spam
//      policy as scaled content abuse regardless of intent.
//
// This is a heuristic gate, not a guarantee of quality — see README >
// Known Limitations. It is intentionally conservative: it is much cheaper
// to leave a page in PENDING_REVIEW for a week than to publish something
// thin.

const PASS_THRESHOLD = 70;

const MIN_WORDS: Record<string, number> = {
  overview: 60,
  gameplay: 35,
  features: 25,
  tipsAndTricks: 25,
  beginnerGuide: 20,
  howToPlay: 15,
  alternatives: 15,
};

// Generic marketing/filler phrases that show up disproportionately in
// low-effort auto-generated copy. Not exhaustive — extend as you observe
// real failure patterns in JobRun logs.
const FILLER_PHRASES = [
  'revolutioniz',
  'changing the face of gaming',
  "gamer's dream",
  'must-play title',
  'in today\'s gaming landscape',
  'look no further',
  'in this article, we will',
  'in this guide, we will',
  'stay tuned for more updates',
  'in conclusion',
  'unleash your',
  'take your gaming experience to the next level',
];

function wordCount(text: string): number {
  return text.trim().split(/\s+/).filter(Boolean).length;
}

function countFillerHits(allText: string): string[] {
  const lower = allText.toLowerCase();
  return FILLER_PHRASES.filter((phrase) => lower.includes(phrase));
}

// --- Lightweight near-duplicate detection -----------------------------
// A MinHash-*style* sketch, not a rigorous implementation: good enough to
// catch "same template, different game name" pages cheaply, without a
// vector DB. If false positives/negatives become a real problem at scale,
// swap this for real embeddings + a vector index — see README > Phase 2.

function normalize(text: string): string[] {
  return text
    .toLowerCase()
    .replace(/[^a-z0-9\s]/g, ' ')
    .split(/\s+/)
    .filter(Boolean);
}

function shingles(words: string[], k = 5): string[] {
  if (words.length < k) return [words.join(' ')];
  const out: string[] = [];
  for (let i = 0; i <= words.length - k; i++) out.push(words.slice(i, i + k).join(' '));
  return out;
}

function hash32(str: string): number {
  let h = 2166136261;
  for (let i = 0; i < str.length; i++) {
    h ^= str.charCodeAt(i);
    h = Math.imul(h, 16777619);
  }
  return h >>> 0;
}

const SKETCH_SIZE = 48;

export function fingerprintText(text: string): string {
  const grams = shingles(normalize(text));
  const hashes = [...new Set(grams.map(hash32))].sort((a, b) => a - b).slice(0, SKETCH_SIZE);
  return hashes.map((h) => h.toString(36)).join(',');
}

export function fingerprintSimilarity(a: string, b: string): number {
  const setA = new Set(a.split(','));
  const setB = new Set(b.split(','));
  if (setA.size === 0 || setB.size === 0) return 0;
  let intersection = 0;
  for (const x of setA) if (setB.has(x)) intersection++;
  const union = setA.size + setB.size - intersection;
  return union === 0 ? 0 : intersection / union;
}

const SIMILARITY_FAIL_THRESHOLD = 0.55;

// --- Source completeness ------------------------------------------------

export function sourceDataIsSufficientForPublish(facts: GameFacts): { ok: boolean; reason?: string } {
  if (!facts.summary && !facts.storyline) {
    return { ok: false, reason: 'No summary or storyline available from IGDB/RAWG yet' };
  }
  const presentSignals = [
    facts.genres.length > 0,
    facts.platforms.length > 0,
    facts.developers.length > 0 || facts.publishers.length > 0,
    facts.releaseDate !== null,
  ].filter(Boolean).length;

  if (presentSignals < 2) {
    return { ok: false, reason: 'Fewer than 2 of {genres, platforms, companies, release date} present' };
  }
  return { ok: true };
}

// --- Main entry point -----------------------------------------------------

export function evaluateContentQuality(
  facts: GameFacts,
  content: GeneratedContent,
  comparisonFingerprints: string[],
): QualityGateResult {
  const reasons: string[] = [];
  let score = 100;

  const sourceCheck = sourceDataIsSufficientForPublish(facts);
  if (!sourceCheck.ok) {
    reasons.push(`source-data-thin: ${sourceCheck.reason}`);
    score -= 60; // effectively a hard fail on its own
  }

  for (const [field, min] of Object.entries(MIN_WORDS)) {
    const text = (content as unknown as Record<string, string>)[field] ?? '';
    const count = wordCount(text);
    if (count < min) {
      reasons.push(`${field}-too-short: ${count}/${min} words`);
      score -= 8;
    }
  }

  if (content.faqs.length < 3) {
    reasons.push(`too-few-faqs: ${content.faqs.length}`);
    score -= 10;
  } else {
    const thinFaqs = content.faqs.filter((f) => wordCount(f.answer) < 6).length;
    if (thinFaqs > 0) {
      reasons.push(`${thinFaqs}-thin-faq-answers`);
      score -= thinFaqs * 3;
    }
  }

  if (content.pros.length < 3) {
    reasons.push(`too-few-pros: ${content.pros.length}`);
    score -= 5;
  }
  if (content.cons.length < 2) {
    reasons.push(`too-few-cons: ${content.cons.length}`);
    score -= 5;
  }

  const allText = [
    content.overview,
    content.gameplay,
    content.features,
    content.tipsAndTricks,
    content.beginnerGuide,
    content.howToPlay,
    content.alternatives,
  ].join(' ');

  const fillerHits = countFillerHits(allText);
  if (fillerHits.length > 0) {
    reasons.push(`filler-phrases: ${fillerHits.join('; ')}`);
    score -= fillerHits.length * 12;
  }

  const fingerprint = fingerprintText([content.overview, content.gameplay, content.features].join(' '));
  let maxSimilarity = 0;
  for (const existing of comparisonFingerprints) {
    maxSimilarity = Math.max(maxSimilarity, fingerprintSimilarity(fingerprint, existing));
  }
  if (maxSimilarity >= SIMILARITY_FAIL_THRESHOLD) {
    reasons.push(`near-duplicate: ${(maxSimilarity * 100).toFixed(0)}% similar to an existing page`);
    score -= 40;
  }

  score = Math.max(0, Math.min(100, score));

  return {
    score,
    passed: score >= PASS_THRESHOLD && sourceCheck.ok && maxSimilarity < SIMILARITY_FAIL_THRESHOLD,
    reasons,
    fingerprint,
  };
}
