/**
 * SEO Analyzer
 * ─────────────
 * Takes raw page data from the structural scanner and produces a
 * structured SeoFindings object with a weighted health score.
 *
 * Health score weights (documented here for auditability):
 *   Critical (40%): missing H1s, missing meta titles, internal 404s
 *   High     (30%): duplicate meta tags, missing alt text, no schema
 *   Medium   (20%): thin content (<300 words), non-indexable pages, missing meta descriptions
 *   Low      (10%): no sitemap, no robots.txt
 */

import type { PageData, SeoFindings } from "../types";
import type { OnPageSummary } from "../scrapers/dataforseo-onpage";

// ─── Analysis ─────────────────────────────────────────────────────────────────

export function analyzeSite(
  pages: PageData[],
  summary: OnPageSummary,
): { findings: SeoFindings; healthScore: number; scoringBreakdown: Record<string, number> } {

  const htmlPages = pages.filter((p) => p.statusCode >= 200 && p.statusCode < 300);
  const indexablePages = htmlPages.filter((p) => p.isIndexable);

  // ── Meta titles ──────────────────────────────────────────────────────────
  const titles = htmlPages.map((p) => p.title ?? "");
  const missingTitles = titles.filter((t) => !t).length;
  const longTitles = titles.filter((t) => t.length > 60).length;
  const shortTitles = titles.filter((t) => t.length > 0 && t.length < 30).length;
  const avgTitleLen = titles.filter((t) => t).reduce((s, t) => s + t.length, 0) / Math.max(titles.filter((t) => t).length, 1);

  // Duplicates: titles that appear more than once (excluding empty)
  const titleCounts = titles.filter((t) => t).reduce<Record<string, number>>((m, t) => {
    m[t] = (m[t] ?? 0) + 1;
    return m;
  }, {});
  const dupTitles = Object.entries(titleCounts)
    .filter(([, c]) => c > 1)
    .map(([t]) => t)
    .slice(0, 5);

  // ── Meta descriptions ────────────────────────────────────────────────────
  const descs = htmlPages.map((p) => p.metaDescription ?? "");
  const missingDescs = descs.filter((d) => !d).length;
  const longDescs = descs.filter((d) => d.length > 160).length;
  const shortDescs = descs.filter((d) => d.length > 0 && d.length < 70).length;
  const avgDescLen = descs.filter((d) => d).reduce((s, d) => s + d.length, 0) / Math.max(descs.filter((d) => d).length, 1);

  const descCounts = descs.filter((d) => d).reduce<Record<string, number>>((m, d) => {
    m[d] = (m[d] ?? 0) + 1;
    return m;
  }, {});
  const dupDescs = Object.entries(descCounts)
    .filter(([, c]) => c > 1)
    .map(([d]) => d.slice(0, 60) + "…")
    .slice(0, 5);

  // ── Headings ────────────────────────────────────────────────────────────
  const missingH1 = htmlPages.filter((p) => !p.h1).length;
  const pagesWithH1 = htmlPages.filter((p) => !!p.h1).length;

  // ── Broken links ────────────────────────────────────────────────────────
  const broken404 = pages.filter((p) => p.statusCode === 404).map((p) => p.url).slice(0, 20);

  // ── Images ──────────────────────────────────────────────────────────────
  const totalImages = htmlPages.reduce((s, p) => s + p.images.length, 0);
  const missingAlt = htmlPages.reduce(
    (s, p) => s + p.images.filter((img) => !img.alt).length,
    0,
  );

  // ── Schema ──────────────────────────────────────────────────────────────
  const pagesWithSchema = htmlPages.filter((p) => p.hasSchema).length;
  const allSchemaTypes = [...new Set(htmlPages.flatMap((p) => p.schemaTypes))];
  const schemaCoverage = Math.round((pagesWithSchema / Math.max(htmlPages.length, 1)) * 100);

  // Suggest schema types based on what's missing
  const schemaOpportunities: string[] = [];
  if (!allSchemaTypes.includes("LocalBusiness") && !allSchemaTypes.includes("MedicalBusiness")) {
    schemaOpportunities.push("LocalBusiness or Organization");
  }
  if (!allSchemaTypes.includes("FAQPage")) schemaOpportunities.push("FAQPage for FAQ content");
  if (!allSchemaTypes.includes("BreadcrumbList")) schemaOpportunities.push("BreadcrumbList");
  if (!allSchemaTypes.includes("Review") && !allSchemaTypes.includes("AggregateRating")) {
    schemaOpportunities.push("Review / AggregateRating");
  }

  // ── Canonical issues ─────────────────────────────────────────────────────
  const canonicalIssues = htmlPages
    .filter((p) => p.canonical && p.canonical !== p.url && !p.canonical.includes(p.url.split("?")[0]))
    .map((p) => p.url)
    .slice(0, 10);

  // ── Redirects ───────────────────────────────────────────────────────────
  const redirectChains = pages
    .filter((p) => p.redirectTarget && p.statusCode >= 300 && p.statusCode < 400)
    .map((p) => p.url)
    .slice(0, 10);

  // ── Content ─────────────────────────────────────────────────────────────
  // avgWordCount: only pages with a known word count (avoids deflating avg with undefined=0)
  const contentPages = htmlPages.filter((p) => (p.wordCount ?? 0) > 0);
  const avgWordCount = contentPages.length
    ? Math.round(contentPages.reduce((s, p) => s + (p.wordCount ?? 0), 0) / contentPages.length)
    : 0;
  // thinContent: treat undefined wordCount (JS-rendered, no word count returned) as 0 — conservative.
  // Previously guarded with `> 0 &&` which silently excluded these pages from the thin-content penalty.
  const thinContent = htmlPages
    .filter((p) => (p.wordCount ?? 0) < 300)
    .map((p) => p.url)
    .slice(0, 10);

  // ── Top pages by inbound links ───────────────────────────────────────────
  const topPages = [...htmlPages]
    .sort((a, b) => b.internalLinks.length - a.internalLinks.length)
    .slice(0, 5)
    .map((p) => p.url);

  // ─── Findings object ─────────────────────────────────────────────────────

  const findings: SeoFindings = {
    siteSize: {
      totalPages: summary.totalPages || htmlPages.length,
      crawlableDepth: 3,
      hasSitemap: summary.hasSitemap,
      hasRobotsTxt: summary.hasRobotsTxt,
    },
    metaTitles: {
      total: htmlPages.length,
      missing: missingTitles,
      tooLong: longTitles,
      tooShort: shortTitles,
      duplicates: dupTitles,
      averageLength: Math.round(avgTitleLen),
    },
    metaDescriptions: {
      total: htmlPages.length,
      missing: missingDescs,
      tooLong: longDescs,
      tooShort: shortDescs,
      duplicates: dupDescs,
      averageLength: Math.round(avgDescLen),
    },
    headings: {
      missingH1,
      multipleH1: 0,
      pagesWithH1,
      deepStructureIssues: [],
    },
    brokenLinks: {
      internal404s: broken404,
      total: broken404.length,
    },
    images: {
      missingAlt,
      totalImages,
      oversizedCount: 0,
      brokenCount: 0,
    },
    schema: {
      typesDetected: allSchemaTypes,
      pagesWithSchema,
      percentCoverage: schemaCoverage,
      missingOpportunities: schemaOpportunities,
    },
    technical: {
      hasSitemap: summary.hasSitemap,
      hasRobotsTxt: summary.hasRobotsTxt,
      canonicalIssues,
      mobileFriendly: summary.mobileFriendly,
      nonIndexablePages: htmlPages.length - indexablePages.length,
      redirectChains,
    },
    content: {
      avgWordCount,
      thinContentPages: thinContent,
      // Use all HTML pages as denominator so the thin-content ratio reflects actual site coverage,
      // not just pages where the crawler returned a word count.
      totalContentPages: htmlPages.length,
    },
    topPages,
  };

  // ─── Health score ─────────────────────────────────────────────────────────
  // Each component is 0–100; combined with weights.

  const total = Math.max(htmlPages.length, 1);

  // Critical (40%): H1 coverage, title coverage, broken links
  const h1Score = Math.round(((total - missingH1) / total) * 100);
  const titleScore = Math.round(((total - missingTitles) / total) * 100);
  const brokenScore = broken404.length === 0 ? 100 : Math.max(0, 100 - broken404.length * 10);
  const criticalScore = Math.round((h1Score + titleScore + brokenScore) / 3);

  // High (30%): duplicate metas, alt text, schema presence
  const dupPenalty = Math.min(dupTitles.length + dupDescs.length, 10) * 5;
  const dupScore = Math.max(0, 100 - dupPenalty);
  const altScore = totalImages === 0 ? 100 : Math.round(((totalImages - missingAlt) / totalImages) * 100);
  const schemaScore = schemaCoverage >= 30 ? 100 : Math.round(schemaCoverage * 3.3);
  const highScore = Math.round((dupScore + altScore + schemaScore) / 3);

  // Medium (20%): thin content, non-indexable, missing descriptions
  const thinScore = contentPages.length === 0 ? 100 : Math.max(0, 100 - Math.round((thinContent.length / contentPages.length) * 100));
  const indexScore = Math.round((indexablePages.length / total) * 100);
  const descScore = Math.round(((total - missingDescs) / total) * 100);
  const mediumScore = Math.round((thinScore + indexScore + descScore) / 3);

  // Low (10%): sitemap + robots
  const lowScore = (summary.hasSitemap ? 50 : 0) + (summary.hasRobotsTxt ? 50 : 0);

  const healthScore = Math.round(
    criticalScore * 0.4 +
    highScore * 0.3 +
    mediumScore * 0.2 +
    lowScore * 0.1,
  );

  const scoringBreakdown = {
    critical: criticalScore,
    high: highScore,
    medium: mediumScore,
    low: lowScore,
  };

  return { findings, healthScore: Math.min(100, Math.max(0, healthScore)), scoringBreakdown };
}