/**
 * E-E-A-T Audit Service
 * ──────────────────────
 * Detects Experience, Expertise, Authoritativeness, and Trustworthiness signals
 * from crawl and content data. Applies to every brand — never returns null.
 *
 * Internal module — vendor names must not appear in user-facing output.
 */

import type { ContentSample } from "../scrapers/firecrawl-content";
import type { PageData } from "../types";
import type { IndustryContext } from "../synthesizers/industry-classifier";

// ─── Re-export EeatAudit so it's importable from this module ─────────────────

export interface EeatAudit {
  compositeScore: number;
  industryWeight: "critical" | "high" | "moderate" | "low";
  experienceSignals: ExperienceSignals;
  expertiseSignals: ExpertiseSignals;
  authoritativenessSignals: AuthoritativenessSignals;
  trustworthinessSignals: TrustworthinessSignals;
  topGaps: EeatGap[];
  industryContextNarrative: string;
}

export interface ExperienceSignals {
  score: number;
  authorBylinesDetected: number;
  pagesWithAuthorByline: number;
  pagesNeedingAuthorByline: number;
  caseStudiesDetected: boolean;
  customerStoriesDetected: boolean;
  yearsInBusinessMentioned: boolean;
  foundingDateInSchema: boolean;
  detectedSignals: string[];
  missingSignals: string[];
}

export interface ExpertiseSignals {
  score: number;
  credentialedAuthors: number;
  certificationsMentioned: string[];
  licenseNumbersDetected: boolean;
  professionalAssociations: string[];
  hasAboutPageWithCredentials: boolean;
  detectedSignals: string[];
  missingSignals: string[];
}

export interface AuthoritativenessSignals {
  score: number;
  organizationSchemaPresent: boolean;
  sameAsLinksCount: number;
  pressMentionsDetected: boolean;
  awardsDetected: boolean;
  industryRecognitionMentioned: boolean;
  detectedSignals: string[];
  missingSignals: string[];
}

export interface TrustworthinessSignals {
  score: number;
  privacyPolicyPresent: boolean;
  termsOfServicePresent: boolean;
  contactPageWithFullNap: boolean;
  physicalAddressDisclosed: boolean;
  aggregateRatingSchemaPresent: boolean;
  reviewCount: number;
  avgRating: number | null;
  securityIndicatorsPresent: boolean;
  transparentOwnership: boolean;
  detectedSignals: string[];
  missingSignals: string[];
}

export interface EeatGap {
  dimension: "experience" | "expertise" | "authoritativeness" | "trustworthiness";
  gap: string;
  impactDescription: string;
  implementationDifficulty: "easy" | "moderate" | "hard";
  estimatedScoreLift: number;
}

// ─── Industry weight mapping ──────────────────────────────────────────────────

function resolveIndustryWeight(industryContext: IndustryContext | null): EeatAudit["industryWeight"] {
  const id = industryContext?.industry ?? "other";
  if (["healthcare-medical", "healthcare-dental", "finance", "legal"].includes(id)) return "critical";
  if (["higher-ed", "b2b-saas", "real-estate", "nonprofit"].includes(id)) return "high";
  if (["local-services", "hospitality", "ecommerce"].includes(id)) return "moderate";
  return "low";
}

function resolveNarrative(industryContext: IndustryContext | null, weight: EeatAudit["industryWeight"]): string {
  const industry = industryContext?.subVertical ?? industryContext?.industry ?? "this industry";
  switch (weight) {
    case "critical":
      return `E-E-A-T signals are a structural prerequisite for AI citation eligibility in ${industry}. Google and AI engines apply heightened scrutiny to Experience, Expertise, Authoritativeness, and Trustworthiness signals before surfacing content for sensitive queries — gaps in any dimension reduce citation eligibility across all engines.`;
    case "high":
      return `AI engines weight E-E-A-T signals heavily in ${industry}, where buyers rely on search to evaluate credibility before converting. Missing credential signals and organizational authority markers reduce citation eligibility for competitive, high-intent queries.`;
    case "moderate":
      return `E-E-A-T signals meaningfully affect AI citation eligibility in ${industry}, particularly for queries where reviewers and local trust signals differentiate results. Gaps in trustworthiness and authority reduce visibility on review-adjacent queries.`;
    default:
      return `E-E-A-T signals contribute to AI citation eligibility across all industries. Strengthening Experience, Expertise, Authoritativeness, and Trustworthiness signals improves content quality signals for AI engines evaluating source credibility.`;
  }
}

// ─── Experience signal detection ─────────────────────────────────────────────

const AUTHOR_BYLINE_RE = /\bby\s+[A-Z][a-z]+(?:\s+[A-Z][a-z]+){1,3}\b|written\s+by|author[:\s]+[A-Z]/g;
const CASE_STUDY_RE = /case\s+stud(?:y|ies)|success\s+stor(?:y|ies)|client\s+result|before\s+and\s+after/i;
const CUSTOMER_STORY_RE = /customer\s+stor(?:y|ies)|testimonial|patient\s+stor(?:y|ies)|client\s+testimonial/i;
const YEARS_IN_BUSINESS_RE = /(?:serving|in\s+business|established|founded|since)\s+(?:19|20)\d{2}|\d{1,2}\s*\+?\s*years?\s+(?:of\s+)?experience|over\s+\d{1,2}\s+years/i;
const FOUNDING_DATE_SCHEMA_RE = /"foundingDate"\s*:/i;

function scoreExperience(pages: PageData[], samples: ContentSample[]): ExperienceSignals {
  const allText = samples.map((s) => s.markdown).join("\n");
  const allPageText = pages.map((p) => p.contentMarkdown ?? "").join("\n");
  const combined = allText + "\n" + allPageText;

  const bylinesPerSample = samples.map((s) => (s.markdown.match(AUTHOR_BYLINE_RE) ?? []).length > 0);
  const authorBylinesDetected = bylinesPerSample.filter(Boolean).length;
  const pagesWithAuthorByline = authorBylinesDetected;
  const pagesNeedingAuthorByline = Math.max(0, samples.length - authorBylinesDetected);

  const caseStudiesDetected = CASE_STUDY_RE.test(combined);
  const customerStoriesDetected = CUSTOMER_STORY_RE.test(combined);
  const yearsInBusinessMentioned = YEARS_IN_BUSINESS_RE.test(combined);

  const allSchema = pages.flatMap((p) => p.schemaTypes ?? []).join(" ");
  const foundingDateInSchema = FOUNDING_DATE_SCHEMA_RE.test(allSchema) ||
    pages.some((p) => p.contentMarkdown && FOUNDING_DATE_SCHEMA_RE.test(p.contentMarkdown));

  const detectedSignals: string[] = [];
  const missingSignals: string[] = [];

  if (authorBylinesDetected > 0) detectedSignals.push(`Author bylines on ${authorBylinesDetected} page(s)`);
  else missingSignals.push("No author bylines detected — add credited authors to content pages");

  if (caseStudiesDetected) detectedSignals.push("Case studies or results content detected");
  else missingSignals.push("No case studies or client results content found");

  if (customerStoriesDetected) detectedSignals.push("Customer or patient stories present");
  else missingSignals.push("No customer testimonials or patient stories detected");

  if (yearsInBusinessMentioned) detectedSignals.push("Years in business or founding date mentioned in content");
  else missingSignals.push("Founding date or years in business not mentioned — add to About page");

  if (foundingDateInSchema) detectedSignals.push("Founding date in schema markup");

  // Score: weighted presence of signals
  let score = 0;
  if (authorBylinesDetected > 0) score += 35;
  if (caseStudiesDetected) score += 20;
  if (customerStoriesDetected) score += 20;
  if (yearsInBusinessMentioned) score += 15;
  if (foundingDateInSchema) score += 10;

  return {
    score: Math.min(100, score),
    authorBylinesDetected,
    pagesWithAuthorByline,
    pagesNeedingAuthorByline,
    caseStudiesDetected,
    customerStoriesDetected,
    yearsInBusinessMentioned,
    foundingDateInSchema,
    detectedSignals,
    missingSignals,
  };
}

// ─── Expertise signal detection ───────────────────────────────────────────────

const CREDENTIAL_RE = /\b(?:M\.?D\.?|D\.?O\.?|Ph\.?D\.?|J\.?D\.?|C\.?P\.?A\.?|C\.?F\.?A\.?|M\.?B\.?A\.?|R\.?N\.?|D\.?D\.?S\.?|D\.?M\.?D\.?|Psy\.?D\.?)\b/g;
const CERTIFICATION_RE = /\bcertif(?:ied|ication)\b|\bboard[\s-]?certif|\baccredited\b/gi;
const LICENSE_RE = /\blicense\s*(?:number|#|no\.?)?\s*:?\s*[A-Z0-9\-]{4,}/i;
const ASSOC_RE = /\bAmeri(?:can)?\b|\bNational\s+Association|\bBoard\s+of\b|\bSociety\s+of\b|\bInstitute\s+of\b/gi;
const ABOUT_CREDENTIAL_RE = /\/about(?:-us)?/i;

function scoreExpertise(pages: PageData[], samples: ContentSample[]): ExpertiseSignals {
  const allText = samples.map((s) => s.markdown).join("\n");
  const combined = allText + "\n" + pages.map((p) => p.contentMarkdown ?? "").join("\n");

  const credentialMatches = combined.match(CREDENTIAL_RE) ?? [];
  const credentialedAuthors = new Set(credentialMatches).size;

  const certMatches = combined.match(CERTIFICATION_RE) ?? [];
  const certificationsMentioned = [...new Set(certMatches.map((c) => c.trim()))].slice(0, 5);

  const licenseNumbersDetected = LICENSE_RE.test(combined);

  const assocMatches = combined.match(ASSOC_RE) ?? [];
  const professionalAssociations = [...new Set(assocMatches.map((a) => a.trim()))].slice(0, 5);

  const aboutSample = samples.find((s) => ABOUT_CREDENTIAL_RE.test(s.url));
  const hasAboutPageWithCredentials = !!(
    aboutSample &&
    (CREDENTIAL_RE.test(aboutSample.markdown) || CERTIFICATION_RE.test(aboutSample.markdown))
  );

  const detectedSignals: string[] = [];
  const missingSignals: string[] = [];

  if (credentialedAuthors > 0) detectedSignals.push(`Professional credentials detected (${credentialedAuthors} credential type(s))`);
  else missingSignals.push("No professional credentials found — add author bios with credentials");

  if (certificationsMentioned.length > 0) detectedSignals.push(`Certifications mentioned: ${certificationsMentioned.slice(0, 3).join(", ")}`);
  else missingSignals.push("No certifications or accreditations mentioned");

  if (licenseNumbersDetected) detectedSignals.push("License numbers referenced in content");
  else missingSignals.push("No license numbers detected — add to footer or About page for regulated industries");

  if (professionalAssociations.length > 0) detectedSignals.push(`Professional association references detected`);
  else missingSignals.push("No professional association memberships mentioned");

  if (hasAboutPageWithCredentials) detectedSignals.push("About page includes professional credentials");
  else missingSignals.push("About page does not surface professional credentials prominently");

  let score = 0;
  if (credentialedAuthors > 0) score += 30;
  if (certificationsMentioned.length > 0) score += 25;
  if (licenseNumbersDetected) score += 20;
  if (professionalAssociations.length > 0) score += 15;
  if (hasAboutPageWithCredentials) score += 10;

  return {
    score: Math.min(100, score),
    credentialedAuthors,
    certificationsMentioned,
    licenseNumbersDetected,
    professionalAssociations,
    hasAboutPageWithCredentials,
    detectedSignals,
    missingSignals,
  };
}

// ─── Authoritativeness signal detection ───────────────────────────────────────

const ORG_SCHEMA_RE = /"@type"\s*:\s*"Organization"/i;
const SAME_AS_RE = /"sameAs"\s*:\s*\[/i;
const PRESS_RE = /(?:featured\s+in|as\s+seen\s+in|covered\s+by|press\s+coverage|in\s+the\s+news|media\s+coverage|mentioned\s+in)/i;
const AWARD_RE = /\bawarded\b|\baward\b|\bbest\s+of\b|\bwinner\b|\bhonored\b|\brecognized\s+(?:as|for|by)\b/i;
const RECOGNITION_RE = /\b(?:ranked|recognized|accredited|designated|endorsed|affiliated)\b/i;

function scoreAuthoritativeness(pages: PageData[], samples: ContentSample[], authorityData: { referringDomains?: number | null } | null): AuthoritativenessSignals {
  const allSchema = pages.map((p) => p.contentMarkdown ?? "").join("\n");
  const allText = samples.map((s) => s.markdown).join("\n");
  const combined = allText + "\n" + allSchema;

  const organizationSchemaPresent = pages.some((p) => p.schemaTypes.includes("Organization") || p.schemaTypes.includes("LocalBusiness") || p.schemaTypes.includes("MedicalOrganization"));

  // Rough sameAs count from any page markup
  const sameAsLinksCount = (combined.match(/"sameAs"/g) ?? []).length;

  const pressMentionsDetected = PRESS_RE.test(combined);
  const awardsDetected = AWARD_RE.test(combined);
  const industryRecognitionMentioned = RECOGNITION_RE.test(combined);

  const detectedSignals: string[] = [];
  const missingSignals: string[] = [];

  if (organizationSchemaPresent) detectedSignals.push("Organization or LocalBusiness schema present");
  else missingSignals.push("No Organization schema — add Organization JSON-LD with sameAs links");

  if (sameAsLinksCount > 0) detectedSignals.push(`${sameAsLinksCount} sameAs link(s) detected in schema`);
  else missingSignals.push("No sameAs links in Organization schema — add Google Business, LinkedIn, Wikidata URLs");

  if (pressMentionsDetected) detectedSignals.push("Press mentions or media coverage referenced");
  else missingSignals.push("No press coverage mentioned — add media mentions to About or Press page");

  if (awardsDetected) detectedSignals.push("Awards or recognition mentioned");
  else missingSignals.push("No awards or industry recognition referenced");

  if (industryRecognitionMentioned) detectedSignals.push("Industry accreditation or recognition detected");

  if (authorityData?.referringDomains && authorityData.referringDomains > 50) {
    detectedSignals.push(`${authorityData.referringDomains} referring domains (authority signal)`);
  }

  let score = 0;
  if (organizationSchemaPresent) score += 25;
  if (sameAsLinksCount > 0) score += Math.min(20, sameAsLinksCount * 7);
  if (pressMentionsDetected) score += 20;
  if (awardsDetected) score += 15;
  if (industryRecognitionMentioned) score += 10;
  if (authorityData?.referringDomains && authorityData.referringDomains > 10) score += Math.min(10, Math.floor(authorityData.referringDomains / 20));

  return {
    score: Math.min(100, score),
    organizationSchemaPresent,
    sameAsLinksCount,
    pressMentionsDetected,
    awardsDetected,
    industryRecognitionMentioned,
    detectedSignals,
    missingSignals,
  };
}

// ─── Trustworthiness signal detection ────────────────────────────────────────

const PRIVACY_RE = /\/privacy(?:-policy)?/i;
const TERMS_RE = /\/terms(?:-of-(?:service|use))?|\/tos\b/i;
const CONTACT_RE = /\/contact(?:-us)?/i;
const PHONE_RE = /(\+1[\s.-]?)?\(?\d{3}\)?[\s.-]?\d{3}[\s.-]?\d{4}/;
const ADDRESS_RE = /\b\d{1,5}\s+[\w\s]+(?:st|ave|rd|dr|blvd|ln|way|ct|pl)\b|\bsuite\s+\d+\b/i;
const AGGREGATE_RATING_RE = /"@type"\s*:\s*"AggregateRating"/i;
const REVIEW_COUNT_RE = /"reviewCount"\s*:\s*(\d+)/i;
const RATING_VALUE_RE = /"ratingValue"\s*:\s*"?([\d.]+)"?/i;
const FOUNDER_RE = /\bfounder\b|\bowner\b|\bceo\b|\bpresident\b/i;
const HTTPS_RE = /^https:\/\//i;

function scoreTrustworthiness(pages: PageData[], samples: ContentSample[]): TrustworthinessSignals {
  const allUrls = [...pages.map((p) => p.url), ...samples.map((s) => s.url)];
  const allText = samples.map((s) => s.markdown).join("\n");
  const allPageText = pages.map((p) => p.contentMarkdown ?? "").join("\n");
  const combined = allText + "\n" + allPageText;
  const externalLinks = pages.flatMap((p) => p.externalLinks ?? []);

  const privacyPolicyPresent = allUrls.some((u) => PRIVACY_RE.test(u)) ||
    externalLinks.some((u) => PRIVACY_RE.test(u)) ||
    /privacy\s+policy/i.test(combined);

  const termsOfServicePresent = allUrls.some((u) => TERMS_RE.test(u)) ||
    /terms\s+of\s+(?:service|use)/i.test(combined);

  const contactPage = samples.find((s) => CONTACT_RE.test(s.url));
  const contactPageWithFullNap = !!(contactPage &&
    PHONE_RE.test(contactPage.markdown) &&
    ADDRESS_RE.test(contactPage.markdown));

  const physicalAddressDisclosed = ADDRESS_RE.test(combined);

  const schemaContent = combined;
  const aggregateRatingSchemaPresent = AGGREGATE_RATING_RE.test(schemaContent);

  const reviewCountMatch = combined.match(REVIEW_COUNT_RE);
  const reviewCount = reviewCountMatch ? parseInt(reviewCountMatch[1], 10) : 0;

  const ratingValueMatch = combined.match(RATING_VALUE_RE);
  const avgRating = ratingValueMatch ? parseFloat(ratingValueMatch[1]) : null;

  const securityIndicatorsPresent = pages.length > 0 && pages.every((p) => HTTPS_RE.test(p.url));

  const transparentOwnership = FOUNDER_RE.test(combined);

  const detectedSignals: string[] = [];
  const missingSignals: string[] = [];

  if (privacyPolicyPresent) detectedSignals.push("Privacy policy present");
  else missingSignals.push("No privacy policy detected — required for AI engine trustworthiness evaluation");

  if (termsOfServicePresent) detectedSignals.push("Terms of service present");
  else missingSignals.push("No terms of service page found");

  if (contactPageWithFullNap) detectedSignals.push("Contact page with full NAP (Name, Address, Phone)");
  else if (physicalAddressDisclosed) detectedSignals.push("Physical address referenced in content");
  else missingSignals.push("No physical address or full NAP detected on contact page");

  if (aggregateRatingSchemaPresent) detectedSignals.push(
    reviewCount > 0
      ? `AggregateRating schema (${reviewCount.toLocaleString()} reviews${avgRating ? ", avg " + avgRating.toFixed(1) : ""})`
      : "AggregateRating schema present"
  );
  else missingSignals.push("No AggregateRating schema — add review count and rating to schema for AI citation eligibility");

  if (securityIndicatorsPresent) detectedSignals.push("HTTPS on all crawled pages");
  else missingSignals.push("Non-HTTPS pages detected — security signals affect trustworthiness assessment");

  if (transparentOwnership) detectedSignals.push("Founder/owner identity disclosed in content");
  else missingSignals.push("No founder or leadership identity detected — transparency improves trustworthiness score");

  let score = 0;
  if (privacyPolicyPresent) score += 15;
  if (termsOfServicePresent) score += 10;
  if (contactPageWithFullNap) score += 15;
  else if (physicalAddressDisclosed) score += 8;
  if (aggregateRatingSchemaPresent) score += 25;
  if (reviewCount > 100) score += 10;
  if (securityIndicatorsPresent) score += 15;
  if (transparentOwnership) score += 10;

  return {
    score: Math.min(100, score),
    privacyPolicyPresent,
    termsOfServicePresent,
    contactPageWithFullNap,
    physicalAddressDisclosed,
    aggregateRatingSchemaPresent,
    reviewCount,
    avgRating,
    securityIndicatorsPresent,
    transparentOwnership,
    detectedSignals,
    missingSignals,
  };
}

// ─── Composite score ──────────────────────────────────────────────────────────

function computeComposite(
  exp: number,
  expt: number,
  auth: number,
  trust: number,
  weight: EeatAudit["industryWeight"],
): number {
  // Equal 25% weighting as baseline; industry weight amplifies gap severity (not the math)
  const raw = exp * 0.25 + expt * 0.25 + auth * 0.25 + trust * 0.25;
  return Math.round(raw);
}

// ─── Top gaps derivation ──────────────────────────────────────────────────────

function deriveTopGaps(
  exp: ExperienceSignals,
  expt: ExpertiseSignals,
  auth: AuthoritativenessSignals,
  trust: TrustworthinessSignals,
): EeatGap[] {
  const candidates: EeatGap[] = [];

  if (exp.authorBylinesDetected === 0) {
    candidates.push({
      dimension: "experience",
      gap: "No author bylines on content pages",
      impactDescription: "Author attribution is a prerequisite for AI engines to establish content provenance and source credibility.",
      implementationDifficulty: "easy",
      estimatedScoreLift: 8,
    });
  }

  if (!exp.caseStudiesDetected) {
    candidates.push({
      dimension: "experience",
      gap: "No case studies or documented client results",
      impactDescription: "Experience signals require demonstrated outcomes. Case studies are the most credible format for AI engines evaluating first-hand experience.",
      implementationDifficulty: "moderate",
      estimatedScoreLift: 5,
    });
  }

  if (expt.credentialedAuthors === 0) {
    candidates.push({
      dimension: "expertise",
      gap: "No professional credentials surfaced in content",
      impactDescription: "AI engines evaluating expertise require credentialed authors or license references — especially for YMYL queries.",
      implementationDifficulty: "easy",
      estimatedScoreLift: 7,
    });
  }

  if (!expt.hasAboutPageWithCredentials) {
    candidates.push({
      dimension: "expertise",
      gap: "About page lacks professional credentials",
      impactDescription: "AI engines check About pages for expertise signals. Missing credentials reduce citation eligibility for authoritative queries.",
      implementationDifficulty: "easy",
      estimatedScoreLift: 6,
    });
  }

  if (!auth.organizationSchemaPresent) {
    candidates.push({
      dimension: "authoritativeness",
      gap: "No Organization or LocalBusiness schema",
      impactDescription: "Organization schema with sameAs links is the primary machine-readable authority signal AI engines parse for citation eligibility.",
      implementationDifficulty: "easy",
      estimatedScoreLift: 9,
    });
  }

  if (auth.sameAsLinksCount === 0) {
    candidates.push({
      dimension: "authoritativeness",
      gap: "No sameAs external authority links in schema",
      impactDescription: "sameAs links connect the brand entity to external authority sources (Google Business, LinkedIn, industry directories). Missing links weaken entity disambiguation for AI engines.",
      implementationDifficulty: "easy",
      estimatedScoreLift: 7,
    });
  }

  if (!auth.pressMentionsDetected) {
    candidates.push({
      dimension: "authoritativeness",
      gap: "No press or media coverage referenced",
      impactDescription: "Third-party media mentions are a top authoritativeness signal. Adding a press page or media mention callouts improves authority assessment.",
      implementationDifficulty: "moderate",
      estimatedScoreLift: 5,
    });
  }

  if (!trust.aggregateRatingSchemaPresent) {
    candidates.push({
      dimension: "trustworthiness",
      gap: "No AggregateRating schema",
      impactDescription: "AggregateRating schema is a direct trustworthiness signal. Reviews carousel eligibility and trust assessments both require it.",
      implementationDifficulty: "easy",
      estimatedScoreLift: 8,
    });
  }

  if (!trust.privacyPolicyPresent) {
    candidates.push({
      dimension: "trustworthiness",
      gap: "No privacy policy detected",
      impactDescription: "Absence of a privacy policy is a hard disqualifier for trustworthiness evaluation in regulated industries.",
      implementationDifficulty: "easy",
      estimatedScoreLift: 6,
    });
  }

  if (!trust.transparentOwnership) {
    candidates.push({
      dimension: "trustworthiness",
      gap: "Founder or leadership identity not disclosed",
      impactDescription: "Transparent ownership is a trustworthiness signal. AI engines and quality raters reward brands that disclose who operates the site.",
      implementationDifficulty: "easy",
      estimatedScoreLift: 5,
    });
  }

  return candidates.sort((a, b) => b.estimatedScoreLift - a.estimatedScoreLift).slice(0, 5);
}

// ─── Main export ──────────────────────────────────────────────────────────────

export function aggregateEeatAudit(
  industryContext: IndustryContext | null,
  crawlData: PageData[],
  firecrawlPages: ContentSample[],
  authorityData: { referringDomains?: number | null } | null = null,
): EeatAudit {
  const industryWeight = resolveIndustryWeight(industryContext);
  const industryContextNarrative = resolveNarrative(industryContext, industryWeight);

  const experienceSignals = scoreExperience(crawlData, firecrawlPages);
  const expertiseSignals = scoreExpertise(crawlData, firecrawlPages);
  const authoritativenessSignals = scoreAuthoritativeness(crawlData, firecrawlPages, authorityData);
  const trustworthinessSignals = scoreTrustworthiness(crawlData, firecrawlPages);

  const compositeScore = computeComposite(
    experienceSignals.score,
    expertiseSignals.score,
    authoritativenessSignals.score,
    trustworthinessSignals.score,
    industryWeight,
  );

  const topGaps = deriveTopGaps(
    experienceSignals,
    expertiseSignals,
    authoritativenessSignals,
    trustworthinessSignals,
  );

  return {
    compositeScore,
    industryWeight,
    experienceSignals,
    expertiseSignals,
    authoritativenessSignals,
    trustworthinessSignals,
    topGaps,
    industryContextNarrative,
  };
}
