/**
 * Core research engine for competitor analysis.
 *
 * Uses fetch + cheerio to scrape competitor websites and extract
 * structured intelligence across pricing, features, content, social, and SEO.
 *
 * All network operations are wrapped in try/catch with timeouts —
 * partial data is always returned rather than throwing.
 *
 * @module research/engine
 */

import * as cheerio from "cheerio";
import type {
  CompetitorAnalysis,
  CompetitorEntry,
  ContentStrategy,
  FocusArea,
  PricingInfo,
  PositionQuadrant,
  ResearchBrief,
  ResearchResult,
  SeoMetrics,
  SocialPresence,
} from "../types.js";
import { SPECIFIC_FOCUS_AREAS } from "../types.js";

/** Default fetch timeout in milliseconds */
const FETCH_TIMEOUT_MS = 10_000;

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

/**
 * Fetch a URL with a timeout. Returns null on any failure.
 */
async function safeFetch(url: string): Promise<string | null> {
  try {
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
    const res = await fetch(url, {
      signal: controller.signal,
      headers: {
        "User-Agent":
          "Mozilla/5.0 (compatible; CompetitorResearchBot/1.0; +https://github.com/competitor-research-mcp)",
        Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
      },
      redirect: "follow",
    });
    clearTimeout(timer);
    if (!res.ok) return null;
    return await res.text();
  } catch {
    return null;
  }
}

/**
 * Normalise a base URL so we can build sub-page URLs from it.
 */
function baseUrl(url: string): string {
  try {
    const u = new URL(url);
    return `${u.protocol}//${u.host}`;
  } catch {
    return url.replace(/\/+$/, "");
  }
}

/** Deduplicate and trim a string array */
function unique(arr: string[]): string[] {
  return [...new Set(arr.map((s) => s.trim()).filter(Boolean))];
}

// ─── Social link detection ──────────────────────────────────────────────────

const SOCIAL_PLATFORMS: Record<string, RegExp> = {
  Twitter: /twitter\.com|x\.com/i,
  LinkedIn: /linkedin\.com/i,
  Facebook: /facebook\.com/i,
  Instagram: /instagram\.com/i,
  YouTube: /youtube\.com/i,
  GitHub: /github\.com/i,
  TikTok: /tiktok\.com/i,
};

// ─── Focus area extractors ──────────────────────────────────────────────────

/**
 * Extract pricing information from the competitor's website.
 * Tries /pricing, /plans, /pricing.html paths.
 */
async function extractPricing(base: string, $main: cheerio.CheerioAPI): Promise<PricingInfo | undefined> {
  // Try dedicated pricing pages
  const pricingPaths = ["/pricing", "/plans", "/pricing.html", "/plans.html"];
  let $pricing: cheerio.CheerioAPI | null = null;

  for (const p of pricingPaths) {
    const html = await safeFetch(`${base}${p}`);
    if (html) {
      $pricing = cheerio.load(html);
      break;
    }
  }

  // Also check the main page for pricing links or sections
  const $ = $pricing ?? $main;
  const tiers: PricingInfo["tiers"] = [];

  // Look for common pricing card patterns
  const pricingSelectors = [
    '[class*="pricing"] [class*="card"]',
    '[class*="pricing"] [class*="tier"]',
    '[class*="pricing"] [class*="plan"]',
    '[class*="plan"] [class*="card"]',
    '[data-pricing]',
    ".pricing-card",
    ".price-card",
    ".plan-card",
  ];

  for (const sel of pricingSelectors) {
    $(sel).each((_, el) => {
      const card = $(el);
      const name =
        card.find("h2, h3, h4, [class*='name'], [class*='title']").first().text().trim() ||
        "Unknown Tier";
      const price =
        card.find("[class*='price'], [class*='amount'], .price").first().text().trim() ||
        "Contact sales";
      const features: string[] = [];
      card.find("li, [class*='feature']").each((_, feat) => {
        const text = $(feat).text().trim();
        if (text && text.length < 200) features.push(text);
      });
      if (name !== "Unknown Tier" || features.length > 0) {
        tiers.push({ name, price, features: features.slice(0, 15) });
      }
    });
    if (tiers.length > 0) break;
  }

  // Fallback: look for any price-like patterns on page
  if (tiers.length === 0) {
    const priceRegex = /\$[\d,.]+(?:\/\s*(?:mo|month|year|yr|user))?/gi;
    const pageText = $.text();
    const priceMatches = pageText.match(priceRegex);
    if (priceMatches && priceMatches.length > 0) {
      const uniquePrices = unique(priceMatches).slice(0, 5);
      uniquePrices.forEach((price, i) => {
        tiers.push({ name: `Tier ${i + 1}`, price, features: [] });
      });
    }
  }

  return tiers.length > 0 ? { tiers } : undefined;
}

/**
 * Extract feature lists from the competitor's website.
 */
async function extractFeatures(base: string, $main: cheerio.CheerioAPI): Promise<string[]> {
  const features: string[] = [];

  // Try /features page
  const featuresPaths = ["/features", "/features.html", "/product"];
  let $features: cheerio.CheerioAPI | null = null;

  for (const p of featuresPaths) {
    const html = await safeFetch(`${base}${p}`);
    if (html) {
      $features = cheerio.load(html);
      break;
    }
  }

  const $ = $features ?? $main;

  // Extract from feature sections
  const featureSelectors = [
    '[class*="feature"] h2, [class*="feature"] h3, [class*="feature"] h4',
    '[class*="benefit"] h2, [class*="benefit"] h3',
    '[class*="capability"] h2, [class*="capability"] h3',
    ".feature-title",
    "[data-feature]",
  ];

  for (const sel of featureSelectors) {
    $(sel).each((_, el) => {
      const text = $(el).text().trim();
      if (text && text.length > 2 && text.length < 150) {
        features.push(text);
      }
    });
  }

  // Fallback: use main page h2/h3 headings as feature proxies
  if (features.length === 0) {
    $main("h2, h3").each((_, el) => {
      const text = $main(el).text().trim();
      if (text && text.length > 3 && text.length < 120) {
        features.push(text);
      }
    });
  }

  return unique(features).slice(0, 20);
}

/**
 * Analyse the competitor's content/blog strategy.
 */
async function extractContent(base: string): Promise<ContentStrategy | undefined> {
  const blogPaths = ["/blog", "/blog/", "/articles", "/resources", "/news"];
  let $blog: cheerio.CheerioAPI | null = null;

  for (const p of blogPaths) {
    const html = await safeFetch(`${base}${p}`);
    if (html) {
      $blog = cheerio.load(html);
      break;
    }
  }

  if (!$blog) return undefined;

  const topTopics: string[] = [];
  const contentTypes: string[] = [];

  // Extract blog post titles as topics
  $blog("article h2, article h3, .post-title, [class*='post'] h2, [class*='post'] h3, [class*='blog'] h2, [class*='article'] h2").each(
    (_, el) => {
      const text = $blog!(el).text().trim();
      if (text && text.length > 5 && text.length < 200) {
        topTopics.push(text);
      }
    },
  );

  // Fallback: just grab headings
  if (topTopics.length === 0) {
    $blog("h1, h2, h3").each((_, el) => {
      const text = $blog!(el).text().trim();
      if (text && text.length > 5 && text.length < 200) {
        topTopics.push(text);
      }
    });
  }

  // Detect content types by looking for tags/categories
  $blog('[class*="tag"], [class*="category"], [class*="label"]').each((_, el) => {
    const text = $blog!(el).text().trim();
    if (text && text.length > 1 && text.length < 50) {
      contentTypes.push(text);
    }
  });

  // Estimate blog frequency from post count on page
  const postCount = Math.max(topTopics.length, 1);
  let blogFrequency: string;
  if (postCount >= 10) blogFrequency = "Very active (10+ recent posts visible)";
  else if (postCount >= 5) blogFrequency = "Active (5-10 recent posts visible)";
  else if (postCount >= 2) blogFrequency = "Moderate (2-5 recent posts visible)";
  else blogFrequency = "Low activity (1 or fewer posts visible)";

  return {
    blogFrequency,
    topTopics: unique(topTopics).slice(0, 10),
    contentTypes: unique(contentTypes).slice(0, 10) || ["Blog posts"],
  };
}

/**
 * Detect social media presence from website links.
 */
function extractSocial($: cheerio.CheerioAPI): SocialPresence[] | undefined {
  const presences: SocialPresence[] = [];

  $("a[href]").each((_, el) => {
    const href = $(el).attr("href");
    if (!href) return;

    for (const [platform, regex] of Object.entries(SOCIAL_PLATFORMS)) {
      if (regex.test(href) && !presences.some((p) => p.platform === platform)) {
        presences.push({ platform });
      }
    }
  });

  return presences.length > 0 ? presences : undefined;
}

/**
 * Extract SEO-relevant metrics from the page.
 */
function extractSeo($: cheerio.CheerioAPI, url: string): SeoMetrics | undefined {
  const topKeywords: string[] = [];

  // Meta keywords
  const metaKeywords = $('meta[name="keywords"]').attr("content");
  if (metaKeywords) {
    topKeywords.push(...metaKeywords.split(",").map((k) => k.trim()));
  }

  // Meta description for keyword extraction
  const metaDesc = $('meta[name="description"]').attr("content") ?? "";

  // Extract h1 tags as keyword signals
  $("h1").each((_, el) => {
    const text = $(el).text().trim();
    if (text && text.length < 100) topKeywords.push(text);
  });

  // Title tag
  const title = $("title").text().trim();
  if (title) topKeywords.push(title);

  // Use meta description words as keyword signals (basic extraction)
  if (metaDesc) {
    const words = metaDesc
      .split(/\s+/)
      .filter((w) => w.length > 4)
      .slice(0, 5);
    topKeywords.push(...words);
  }

  // OG tags
  const ogTitle = $('meta[property="og:title"]').attr("content");
  if (ogTitle) topKeywords.push(ogTitle);

  return {
    topKeywords: unique(topKeywords).slice(0, 15),
    estimatedTraffic: "Unknown (requires third-party API)",
    domainAuthority: "Unknown (requires third-party API)",
  };
}

// ─── Strength / weakness heuristics ────────────────────────────────────────

/**
 * Derive strengths and weaknesses from the extracted data.
 */
function deriveStrengthsWeaknesses(analysis: Partial<CompetitorAnalysis>): {
  strengths: string[];
  weaknesses: string[];
} {
  const strengths: string[] = [];
  const weaknesses: string[] = [];

  // Features
  if (analysis.features && analysis.features.length >= 10) {
    strengths.push("Comprehensive feature set with many documented capabilities");
  } else if (analysis.features && analysis.features.length > 0) {
    strengths.push("Has documented feature set");
  } else {
    weaknesses.push("Features are not clearly documented or discoverable on website");
  }

  // Pricing
  if (analysis.pricing && analysis.pricing.tiers.length >= 3) {
    strengths.push("Multiple pricing tiers offer flexibility for different customer segments");
  } else if (analysis.pricing && analysis.pricing.tiers.length > 0) {
    strengths.push("Has transparent pricing");
  } else {
    weaknesses.push("Pricing is not transparent — may create friction for prospects");
  }

  // Content
  if (analysis.contentStrategy) {
    const { topTopics, blogFrequency } = analysis.contentStrategy;
    if (topTopics.length >= 5) {
      strengths.push("Active content marketing with diverse topic coverage");
    }
    if (blogFrequency?.includes("Very active") || blogFrequency?.includes("Active")) {
      strengths.push("Consistent blog publishing cadence");
    } else if (blogFrequency?.includes("Low")) {
      weaknesses.push("Blog appears inactive or rarely updated");
    }
  } else {
    weaknesses.push("No blog or content hub found — missing SEO and thought leadership opportunity");
  }

  // Social
  if (analysis.socialPresence && analysis.socialPresence.length >= 3) {
    strengths.push(`Active on ${analysis.socialPresence.length} social platforms`);
  } else if (analysis.socialPresence && analysis.socialPresence.length > 0) {
    strengths.push(
      `Present on ${analysis.socialPresence.map((s) => s.platform).join(", ")}`,
    );
  } else {
    weaknesses.push("No social media presence detected from website");
  }

  // SEO
  if (analysis.seoMetrics) {
    if (analysis.seoMetrics.topKeywords.length >= 5) {
      strengths.push("Good keyword coverage in meta tags and headings");
    } else {
      weaknesses.push("Limited keyword optimization in meta tags");
    }
  }

  return { strengths, weaknesses };
}

// ─── Public API ─────────────────────────────────────────────────────────────

/**
 * Search the web using Brave Search API.
 *
 * Falls back gracefully to an empty array if BRAVE_API_KEY is not set.
 *
 * @param query - Search query string
 * @returns Array of search results with title, url, and snippet
 */
export async function webSearch(
  query: string,
): Promise<{ title: string; url: string; snippet: string }[]> {
  const apiKey = process.env.BRAVE_API_KEY;
  if (!apiKey) {
    console.warn("[research] BRAVE_API_KEY not set — web search unavailable");
    return [];
  }

  try {
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);

    const url = new URL("https://api.search.brave.com/res/v1/web/search");
    url.searchParams.set("q", query);
    url.searchParams.set("count", "10");

    const res = await fetch(url.toString(), {
      signal: controller.signal,
      headers: {
        Accept: "application/json",
        "Accept-Encoding": "gzip",
        "X-Subscription-Token": apiKey,
      },
    });
    clearTimeout(timer);

    if (!res.ok) {
      console.error(`[research] Brave Search returned ${res.status}`);
      return [];
    }

    const data = (await res.json()) as {
      web?: { results?: { title: string; url: string; description: string }[] };
    };

    return (
      data.web?.results?.map((r) => ({
        title: r.title,
        url: r.url,
        snippet: r.description,
      })) ?? []
    );
  } catch (err) {
    console.error("[research] Brave Search error:", err);
    return [];
  }
}

/**
 * Research a single competitor across the requested focus areas.
 *
 * All network requests are wrapped in try/catch — partial data is returned
 * rather than throwing on individual failures.
 *
 * @param competitor - The competitor to research
 * @param focusAreas - Which aspects to investigate
 * @returns Structured analysis of the competitor
 */
export async function researchCompetitor(
  competitor: CompetitorEntry,
  focusAreas: FocusArea[],
): Promise<CompetitorAnalysis> {
  const areas = focusAreas.includes("all") ? SPECIFIC_FOCUS_AREAS : focusAreas;
  const base = baseUrl(competitor.url);

  // Fetch main page
  const mainHtml = await safeFetch(competitor.url);
  const $main = cheerio.load(mainHtml ?? "<html></html>");

  const analysis: Partial<CompetitorAnalysis> = {
    competitor,
    features: [],
    strengths: [],
    weaknesses: [],
  };

  // Run each focus area extractor
  const extractions = await Promise.allSettled(
    areas.map(async (area) => {
      switch (area) {
        case "pricing":
          analysis.pricing = await extractPricing(base, $main);
          break;
        case "features":
          analysis.features = await extractFeatures(base, $main);
          break;
        case "content":
          analysis.contentStrategy = await extractContent(base);
          break;
        case "social":
          analysis.socialPresence = extractSocial($main);
          break;
        case "seo":
          analysis.seoMetrics = extractSeo($main, competitor.url);
          break;
      }
    }),
  );

  // Log any failed extractions (but don't throw)
  for (const result of extractions) {
    if (result.status === "rejected") {
      console.warn(`[research] Extraction failed for ${competitor.name}:`, result.reason);
    }
  }

  // Derive strengths & weaknesses from what we found
  const { strengths, weaknesses } = deriveStrengthsWeaknesses(analysis);
  analysis.strengths = strengths;
  analysis.weaknesses = weaknesses;

  return analysis as CompetitorAnalysis;
}

/**
 * Run a full competitive analysis across all competitors in the brief.
 *
 * Uses Promise.allSettled for resilience — if one competitor's research fails,
 * the others still return results.
 *
 * @param brief - The complete research brief
 * @returns Full research result with all analyses, opportunities, and positioning
 */
export async function runFullAnalysis(brief: ResearchBrief): Promise<ResearchResult> {
  // Research all competitors in parallel (resilient)
  const results = await Promise.allSettled(
    brief.competitors.map((c) => researchCompetitor(c, brief.focusAreas)),
  );

  const analyses: CompetitorAnalysis[] = [];
  for (const result of results) {
    if (result.status === "fulfilled") {
      analyses.push(result.value);
    } else {
      console.error("[research] Competitor research failed:", result.reason);
    }
  }

  // Generate opportunities by comparing weaknesses to company strengths
  const opportunities = generateOpportunities(brief, analyses);

  // Calculate market positioning
  const marketPositioning = calculatePositioning(brief, analyses);

  return {
    brief,
    analyses,
    opportunities,
    marketPositioning,
    generatedAt: new Date().toISOString(),
  };
}

// ─── Opportunity & positioning logic ────────────────────────────────────────

/**
 * Generate opportunities by cross-referencing competitor weaknesses
 * with the company's value propositions.
 */
function generateOpportunities(
  brief: ResearchBrief,
  analyses: CompetitorAnalysis[],
): string[] {
  const opportunities: string[] = [];
  const allWeaknesses = analyses.flatMap((a) => a.weaknesses);
  const valueProps = brief.company.valueProps;

  // Cross-reference weaknesses with value props
  if (allWeaknesses.some((w) => w.toLowerCase().includes("pricing"))) {
    opportunities.push(
      "Competitors have opaque pricing — transparent pricing could be a differentiator",
    );
  }

  if (allWeaknesses.some((w) => w.toLowerCase().includes("content") || w.toLowerCase().includes("blog"))) {
    opportunities.push(
      "Content gap: competitors are underinvesting in content marketing — opportunity for thought leadership",
    );
  }

  if (allWeaknesses.some((w) => w.toLowerCase().includes("social"))) {
    opportunities.push(
      "Social media gap: competitors have weak social presence — community building could differentiate",
    );
  }

  if (allWeaknesses.some((w) => w.toLowerCase().includes("feature"))) {
    opportunities.push(
      "Feature documentation gap: clearly documenting features/capabilities could build trust",
    );
  }

  if (allWeaknesses.some((w) => w.toLowerCase().includes("seo") || w.toLowerCase().includes("keyword"))) {
    opportunities.push(
      "SEO opportunity: competitors have weak keyword optimization — invest in SEO to capture organic traffic",
    );
  }

  // Generic opportunities based on value props
  for (const vp of valueProps) {
    const vpLower = vp.toLowerCase();
    const competitorsCovering = analyses.filter((a) =>
      a.features.some((f) => f.toLowerCase().includes(vpLower)),
    );
    if (competitorsCovering.length === 0) {
      opportunities.push(
        `Unique value prop "${vp}" is not prominently featured by any competitor — lean into this`,
      );
    }
  }

  // If no specific opportunities found, add general ones
  if (opportunities.length === 0) {
    opportunities.push(
      "Differentiate through superior user experience and documentation",
      "Consider competitive pricing strategy based on market analysis",
      "Invest in content marketing to build organic visibility",
    );
  }

  return unique(opportunities);
}

/**
 * Calculate market positioning for scatter plot visualisation.
 *
 * Uses simple heuristics:
 * - X axis: feature richness (based on feature count)
 * - Y axis: market presence (based on pricing tiers + social + content signals)
 */
function calculatePositioning(
  brief: ResearchBrief,
  analyses: CompetitorAnalysis[],
): PositionQuadrant[] {
  const positions: PositionQuadrant[] = [];

  for (const analysis of analyses) {
    // X: feature richness (0-100 scale)
    const featureScore = Math.min(analysis.features.length * 5, 100);

    // Y: market presence (composite score 0-100)
    let presenceScore = 0;
    if (analysis.pricing && analysis.pricing.tiers.length > 0) presenceScore += 25;
    if (analysis.socialPresence && analysis.socialPresence.length > 0)
      presenceScore += analysis.socialPresence.length * 8;
    if (analysis.contentStrategy) {
      presenceScore += Math.min(analysis.contentStrategy.topTopics.length * 5, 25);
    }
    if (analysis.seoMetrics && analysis.seoMetrics.topKeywords.length > 5)
      presenceScore += 15;

    presenceScore = Math.min(presenceScore, 100);

    positions.push({
      name: analysis.competitor.name,
      x: featureScore,
      y: presenceScore,
    });
  }

  // Add the user's company at a default position (can be refined later)
  positions.push({
    name: brief.company.name + " (You)",
    x: 50,
    y: 50,
  });

  return positions;
}
