/**
 * Competitor vs Brand Analysis
 * ────────────────────────────
 * Cross-references competitor pages (from CompetitorPage table) against
 * the brand's own pages (from Site Tag data and GSC keyword data) to
 * produce content gap analysis, head-to-head comparisons, and strategic
 * recommendations.
 *
 * Three categories:
 *   Content Gap  — competitor has a page for this topic, brand does not
 *   Head-to-Head — both have pages on the same topic
 *   Brand Advantage — brand has a page, competitor does not
 */

import { prisma } from "@/lib/prisma";

// ─── Types ──────────────────────────────────────────────────────────────────

export interface ContentGap {
  competitorId: string;
  competitorDomain: string;
  competitorUrl: string;
  competitorTitle: string | null;
  topic: string;
  wordCount: number | null;
  lastmod: string | null;
  firstSeenAt: string;
  /** 0–1. Higher = more confident this is a true gap (no matching brand page). */
  matchConfidence: number;
}

export interface HeadToHead {
  topic: string;
  brandUrl: string;
  brandTitle: string | null;
  competitorId: string;
  competitorDomain: string;
  competitorUrl: string;
  competitorTitle: string | null;
  competitorWordCount: number | null;
  competitorLastmod: string | null;
  /** 0–1. Higher = more confident the two pages cover the same topic.
   *  1.0 = exact normalized match. Lower values come from fuzzy/Jaccard matching. */
  matchConfidence: number;
}

export interface ContentGapAnalysis {
  gaps: ContentGap[];
  headToHead: HeadToHead[];
  brandPageCount: number;
  competitorPageCount: number;
}

// ─── Helpers ────────────────────────────────────────────────────────────────

/**
 * Extract a search-friendly topic string from a URL path.
 * e.g., "/symptoms-of-poor-circulation/" → "symptoms of poor circulation"
 */
function topicFromSlug(url: string): string {
  try {
    const path = new URL(url).pathname;
    return path
      .replace(/\/$/, "")           // trailing slash
      .split("/")
      .pop()!                       // last segment
      .replace(/\.[^.]+$/, "")      // strip extension
      .replace(/[-_]/g, " ")        // dashes/underscores → spaces
      .replace(/\d{4,}/g, "")       // strip long numeric IDs
      .replace(/\s+/g, " ")
      .trim()
      .toLowerCase();
  } catch {
    return url.replace(/[-_/]/g, " ").trim().toLowerCase();
  }
}

/**
 * Normalize a topic for fuzzy matching: lowercase, remove stop words,
 * sort remaining words alphabetically. This lets "symptoms of poor
 * circulation" match "poor circulation symptoms".
 */
const STOP_WORDS = new Set(["a", "an", "the", "of", "for", "in", "on", "at", "to", "and", "or", "is", "are", "was", "were", "be", "been", "our", "your", "my", "its", "with", "from", "by", "about", "how", "what", "when", "where", "why", "which"]);

function normalizeForMatch(topic: string): string {
  return topic
    .toLowerCase()
    .split(/\s+/)
    .filter((w) => w.length > 1 && !STOP_WORDS.has(w))
    .sort()
    .join(" ");
}

/** Word set from a normalized topic — used for Jaccard similarity. */
function wordSet(normalized: string): Set<string> {
  return new Set(normalized.split(/\s+/).filter((w) => w.length > 0));
}

/**
 * Jaccard similarity: |A ∩ B| / |A ∪ B|.
 * Returns 0–1 where 1 = identical word sets.
 */
function jaccard(a: Set<string>, b: Set<string>): number {
  if (a.size === 0 && b.size === 0) return 0;
  let intersection = 0;
  for (const w of a) if (b.has(w)) intersection++;
  const union = a.size + b.size - intersection;
  return union > 0 ? intersection / union : 0;
}

/** Minimum Jaccard score to consider two topics a partial head-to-head match. */
const PARTIAL_MATCH_THRESHOLD = 0.5;

// ─── Main analysis ──────────────────────────────────────────────────────────

/**
 * Run content gap analysis for a brand against all its active competitors
 * (or a specific competitor if competitorId is provided).
 */
export async function analyzeContentGaps(
  brandId: string,
  competitorId?: string,
): Promise<ContentGapAnalysis> {
  // ── 1. Get brand pages from Site Tag + PageRecord ─────────────────
  // PageRecord stores every URL the Site Tag has seen for this brand.
  const brandPages = await prisma.pageRecord.findMany({
    where: { brandId },
    select: { url: true, title: true },
    take: 5000,
  });

  // Build a set of normalized brand topics for fast lookup
  const brandTopics = new Map<string, { url: string; title: string | null }>();
  for (const p of brandPages) {
    const topic = topicFromSlug(p.url.startsWith("http") ? p.url : `https://x.com${p.url}`);
    if (topic.length >= 3) {
      const normalized = normalizeForMatch(topic);
      if (!brandTopics.has(normalized)) {
        brandTopics.set(normalized, { url: p.url, title: p.title });
      }
    }
  }

  // ── 2. Get competitor pages ────────────────────────────────────────
  const compWhere: Record<string, unknown> = { competitor: { brandId, isActive: true }, status: { not: "removed" } };
  if (competitorId) compWhere.competitorId = competitorId;

  const compPages = await prisma.competitorPage.findMany({
    where: compWhere,
    select: {
      competitorId: true, url: true, title: true, wordCount: true,
      lastmod: true, firstSeenAt: true,
      competitor: { select: { domain: true } },
    },
    take: 10000,
  });

  // Pre-compute word sets for all brand topics for Jaccard comparisons
  const brandWordSets = new Map<string, { ws: Set<string>; url: string; title: string | null }>();
  for (const [norm, info] of brandTopics) {
    brandWordSets.set(norm, { ws: wordSet(norm), ...info });
  }

  // ── 3. Classify each competitor page ──────────────────────────────
  const gaps: ContentGap[] = [];
  const headToHead: HeadToHead[] = [];
  let matchLogCount = 0;

  for (const cp of compPages) {
    const topic = topicFromSlug(cp.url);
    if (topic.length < 3) continue; // skip root/index pages

    const normalized = normalizeForMatch(topic);
    const compWords = wordSet(normalized);

    // 1. Try exact normalized match (confidence = 1.0)
    const exactMatch = brandTopics.get(normalized);
    if (exactMatch) {
      headToHead.push({
        topic,
        brandUrl: exactMatch.url,
        brandTitle: exactMatch.title,
        competitorId: cp.competitorId,
        competitorDomain: cp.competitor.domain,
        competitorUrl: cp.url,
        competitorTitle: cp.title,
        competitorWordCount: cp.wordCount,
        competitorLastmod: cp.lastmod?.toISOString() ?? null,
        matchConfidence: 1.0,
      });
      if (matchLogCount < 50) {
        console.log(`[competitor-analysis] H2H exact: "${topic}" ↔ brand "${exactMatch.url}" (1.0)`);
        matchLogCount++;
      }
      continue;
    }

    // 2. Try fuzzy match via Jaccard similarity
    let bestScore = 0;
    let bestBrand: { url: string; title: string | null } | null = null;
    if (compWords.size > 0) {
      for (const [, bInfo] of brandWordSets) {
        const score = jaccard(compWords, bInfo.ws);
        if (score > bestScore) {
          bestScore = score;
          bestBrand = bInfo;
        }
      }
    }

    if (bestScore >= PARTIAL_MATCH_THRESHOLD && bestBrand) {
      const confidence = Math.round(bestScore * 100) / 100;
      headToHead.push({
        topic,
        brandUrl: bestBrand.url,
        brandTitle: bestBrand.title,
        competitorId: cp.competitorId,
        competitorDomain: cp.competitor.domain,
        competitorUrl: cp.url,
        competitorTitle: cp.title,
        competitorWordCount: cp.wordCount,
        competitorLastmod: cp.lastmod?.toISOString() ?? null,
        matchConfidence: confidence,
      });
      if (matchLogCount < 50) {
        console.log(`[competitor-analysis] H2H fuzzy: "${topic}" ↔ brand "${bestBrand.url}" (${confidence})`);
        matchLogCount++;
      }
    } else {
      // No match above threshold → content gap.
      // Gap confidence = 1 - bestScore: the further from any brand topic,
      // the more confident we are this is a true gap.
      const gapConfidence = Math.round((1 - bestScore) * 100) / 100;
      gaps.push({
        competitorId: cp.competitorId,
        competitorDomain: cp.competitor.domain,
        competitorUrl: cp.url,
        competitorTitle: cp.title,
        topic,
        wordCount: cp.wordCount,
        lastmod: cp.lastmod?.toISOString() ?? null,
        firstSeenAt: cp.firstSeenAt.toISOString(),
        matchConfidence: gapConfidence,
      });
    }
  }

  // Sort gaps by most recent first (newer content = more urgent to respond to)
  gaps.sort((a, b) => b.firstSeenAt.localeCompare(a.firstSeenAt));

  return {
    gaps: gaps.slice(0, 200),
    headToHead: headToHead.slice(0, 200),
    brandPageCount: brandPages.length,
    competitorPageCount: compPages.length,
  };
}

/**
 * Get a summary of recent competitor activity for a brand — new, updated,
 * and removed pages across all competitors in the last N days. Used by
 * the Recent Activity feed in the Competitor Intel UI.
 */
export async function getRecentActivity(
  brandId: string,
  days: number = 30,
): Promise<Array<{
  competitorId: string;
  competitorDomain: string;
  url: string;
  title: string | null;
  status: string;
  firstSeenAt: string;
  lastCheckedAt: string;
}>> {
  const since = new Date(Date.now() - days * 86400000);

  const pages = await prisma.competitorPage.findMany({
    where: {
      competitor: { brandId, isActive: true },
      status: { in: ["new", "updated", "removed"] },
      lastCheckedAt: { gte: since },
    },
    select: {
      competitorId: true, url: true, title: true, status: true,
      firstSeenAt: true, lastCheckedAt: true,
      competitor: { select: { domain: true } },
    },
    orderBy: { lastCheckedAt: "desc" },
    take: 100,
  });

  return pages.map((p) => ({
    competitorId: p.competitorId,
    competitorDomain: p.competitor.domain,
    url: p.url,
    title: p.title,
    status: p.status,
    firstSeenAt: p.firstSeenAt.toISOString(),
    lastCheckedAt: p.lastCheckedAt.toISOString(),
  }));
}
