/**
 * Schema Markup Generator Service
 * ────────────────────────────────
 * Generates JSON-LD structured data using brand context + Claude AI.
 * Validates schemas against Google's structured data guidelines.
 */

import { prisma } from "@/lib/prisma";
import { callLlm } from "@/lib/ai-avatar/llm-client";
import { logAiInteraction } from "@/lib/services/ai-interaction-log";

export const SCHEMA_TYPES = [
  { type: "LocalBusiness", description: "Business with physical location" },
  { type: "MedicalBusiness", description: "Healthcare practice or clinic" },
  { type: "FAQPage", description: "Page with Q&A content" },
  { type: "HowTo", description: "Step-by-step instructions" },
  { type: "Service", description: "Service offering page" },
  { type: "Product", description: "Product listing" },
  { type: "Article", description: "Blog post or article" },
  { type: "BreadcrumbList", description: "Navigation breadcrumbs" },
  { type: "Organization", description: "Company info" },
  { type: "WebSite", description: "Site-level info with search" },
  { type: "Review", description: "Customer reviews/testimonials" },
  { type: "Event", description: "Upcoming events" },
  { type: "VideoObject", description: "Video content" },
  { type: "Person", description: "Team member or author" },
];

export interface SchemaResult {
  recommendedTypes: string[];
  schemas: Array<{ type: string; jsonLd: Record<string, unknown> }>;
  reasoning: string;
}

function extractJson(raw: string): string {
  let s = raw.trim().replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/i, "");
  const f = s.indexOf("{"), l = s.lastIndexOf("}");
  if (f >= 0 && l > f) s = s.slice(f, l + 1);
  return s;
}

async function fetchPageMeta(url: string): Promise<{ title: string; description: string; h1: string }> {
  try {
    const res = await fetch(url, { signal: AbortSignal.timeout(8000), headers: { "User-Agent": "meSEO-Schema/1.0" } });
    if (!res.ok) return { title: "", description: "", h1: "" };
    const html = await res.text();
    const titleMatch = html.match(/<title[^>]*>([^<]+)<\/title>/i);
    const descMatch = html.match(/<meta[^>]*name=["']description["'][^>]*content=["']([^"']+)["']/i);
    const h1Match = html.match(/<h1[^>]*>([^<]+)<\/h1>/i);
    return { title: titleMatch?.[1] || "", description: descMatch?.[1] || "", h1: h1Match?.[1] || "" };
  } catch { return { title: "", description: "", h1: "" }; }
}

export async function generateSchema(brandId: string, pageUrl: string, schemaType?: string): Promise<SchemaResult> {
  const brand = await prisma.brand.findUnique({
    where: { id: brandId },
    include: { profile: true },
  });
  if (!brand) throw new Error("Brand not found");

  const gbpLocation = await prisma.gbpLocation.findFirst({
    where: { brandId, isActive: true },
    select: { locationName: true, address: true, city: true, state: true, zip: true, phone: true, avgRating: true, totalReviews: true, category: true },
  }).catch(() => null);

  const pageMeta = await fetchPageMeta(pageUrl.startsWith("http") ? pageUrl : `https://${brand.domain}${pageUrl}`);
  const validTypes = SCHEMA_TYPES.map((t) => t.type).join(", ");

  const prompt = `Generate JSON-LD schema markup for this page.

Business: ${brand.name}, ${brand.industry || "general"}
Domain: ${brand.domain}
${gbpLocation ? `Location: ${gbpLocation.address || ""}, ${gbpLocation.city || ""} ${gbpLocation.state || ""} ${gbpLocation.zip || ""}` : ""}
${gbpLocation?.phone ? `Phone: ${gbpLocation.phone}` : ""}
${gbpLocation?.avgRating ? `Rating: ${gbpLocation.avgRating} (${gbpLocation.totalReviews} reviews)` : ""}
${gbpLocation?.category ? `Category: ${gbpLocation.category}` : ""}
${brand.profile?.services?.length ? `Services: ${brand.profile.services.join(", ")}` : ""}

Page URL: ${pageUrl}
Page title: ${pageMeta.title || "(unknown)"}
Page H1: ${pageMeta.h1 || "(unknown)"}
Page meta description: ${pageMeta.description || "(unknown)"}

${schemaType ? `Generate specifically: ${schemaType} schema.` : `Recommend the best schema types for this page from: ${validTypes}`}

Return ONLY JSON:
{"recommendedTypes":["LocalBusiness"],"schemas":[{"type":"LocalBusiness","jsonLd":{"@context":"https://schema.org","@type":"LocalBusiness","name":"...","address":{"@type":"PostalAddress","streetAddress":"..."}}}],"reasoning":"why these types fit"}

Use REAL business data. Every schema must have @context and @type.`;

  const result = await callLlm(
    [{ role: "system", content: "You are a structured data expert. Respond ONLY with valid JSON." }, { role: "user", content: prompt }],
    () => JSON.stringify({
      recommendedTypes: [schemaType || "LocalBusiness"],
      schemas: [{ type: schemaType || "LocalBusiness", jsonLd: { "@context": "https://schema.org", "@type": schemaType || "LocalBusiness", name: brand.name, url: `https://${brand.domain}` } }],
      reasoning: "Mock response — configure ANTHROPIC_API_KEY for real generation.",
    }),
    { maxTokens: 4000 },
  );

  logAiInteraction({
    brandId,
    feature: "schema_gen",
    userPrompt: prompt,
    aiResponse: result.reply,
    model: result.model,
    brandDomain: brand.domain,
  });

  try {
    return JSON.parse(extractJson(result.reply)) as SchemaResult;
  } catch {
    return {
      recommendedTypes: [schemaType || "LocalBusiness"],
      schemas: [{ type: schemaType || "LocalBusiness", jsonLd: { "@context": "https://schema.org", "@type": schemaType || "LocalBusiness", name: brand.name } }],
      reasoning: "AI returned invalid JSON — using defaults.",
    };
  }
}

// ─── Brief-schema generation ────────────────────────────────────────────────

/** Input shape for generating JSON-LD off a Content Brief (SEO or
 *  LLMO). Differs from generateSchema() in that no published page
 *  URL exists yet — the user hasn't written the content; they're
 *  planning it. Fields pulled from the brief are passed as first-
 *  class inputs so the LLM has concrete title / description /
 *  keyword / content-type to build the schema around. */
export interface BriefSchemaInput {
  /** Brief flavour — drives the reasoning prompt so an LLMO brief
   *  gets citation-friendly schema hints (Article + SpeakableSpec)
   *  while an SEO brief gets the conventional structure. */
  briefType: "seo" | "llmo";
  /** Recommended page title from the brief. Becomes schema.name /
   *  schema.headline depending on @type. */
  title?: string;
  /** Recommended meta description. Becomes schema.description. */
  description?: string;
  /** Target search keyword from the brief's keyword research. */
  keyword?: string;
  /** Short-list of secondary keywords — used as schema.keywords. */
  secondaryKeywords?: string[];
  /** Brief mode — used as a signal for intent (e.g. "humanized"
   *  mode tends to pair with BlogPosting, "standard" is more
   *  agnostic). Optional, defaults to standard. */
  mode?: "standard" | "humanized";
  /** User-selected page type override. When omitted, we let the AI
   *  auto-pick from the available SCHEMA_TYPES via contentSignal. */
  forceSchemaType?: string;
  /** Free-form text for the AI to classify page type if no
   *  explicit hint is available. Can be the first H1, the brief's
   *  intro, or the target keyword. */
  contentSignal?: string;
}

export interface BriefSchemaResult {
  schemaType: string;
  jsonLd: Record<string, unknown>;
  reasoning: string;
  /** Short text explaining why this particular @type was chosen.
   *  Different from `reasoning` — this is about the TYPE choice,
   *  not the field-filling. Users see it inline under the detected
   *  type badge so they understand the selection. */
  typeReasoning: string;
}

/** Heuristic type inference from a text signal. Runs client-side-ish
 *  (pure string matching) before we spend a Claude call, so the
 *  prompt gets a pre-filtered candidate list to pick from. Returns
 *  the most likely schema.org @type given the text. Falls through
 *  to an Article default so SEO briefs without a specific signal
 *  still get a sensible schema. */
function inferSchemaTypeFromSignal(signal: string, briefType: "seo" | "llmo"): string {
  const s = signal.toLowerCase();
  // FAQ signals are strongest — explicit Q&A language should ALWAYS
  // win over more general categories.
  if (/\bfaq\b|frequently asked|common questions|questions and answers/.test(s)) return "FAQPage";
  // How-to / instructional content.
  if (/\bhow to\b|\bsteps?\b|\bguide\b|\btutorial\b/.test(s)) return "HowTo";
  // Location / near-me — LocalBusiness is the workhorse.
  if (/\bnear me\b|\bin [a-z ]+\b|\blocation\b|\baddress\b|\bhours\b/.test(s)) return "LocalBusiness";
  // Service-page signals.
  if (/\bservice\b|\btreatment\b|\bprocedure\b|\bwhat we offer\b/.test(s)) return "Service";
  // Medical-specific content on LLMO flavour — SpeakableSpec-ready
  // structured data is more useful than generic Article for AI
  // citation engines.
  if (briefType === "llmo" && /\b(cardio|derm|pediatr|ortho|oncol|neurol|clinic|medical|health)\b/.test(s)) {
    return "MedicalWebPage";
  }
  if (/\b(cardio|derm|pediatr|ortho|oncol|neurol|clinic|medical|health)\b/.test(s)) {
    return "MedicalWebPage";
  }
  // Default: Blog/Article. LLMO briefs lean Article (citation-optimised
  // with author + datePublished); SEO briefs lean BlogPosting (has
  // richer article-header metadata that Google's rich results show).
  return briefType === "llmo" ? "Article" : "BlogPosting";
}

export async function generateBriefSchema(brandId: string, input: BriefSchemaInput): Promise<BriefSchemaResult> {
  const brand = await prisma.brand.findUnique({
    where: { id: brandId },
    include: { profile: true },
  });
  if (!brand) throw new Error("Brand not found");

  // GBP location is the highest-confidence local signal when the
  // inferred type is LocalBusiness or MedicalWebPage. Pulled best-
  // effort — a missing GBP row doesn't block generation.
  const gbpLocation = await prisma.gbpLocation.findFirst({
    where: { brandId, isActive: true },
    select: { locationName: true, address: true, city: true, state: true, zip: true, phone: true, avgRating: true, totalReviews: true, category: true },
  }).catch(() => null);

  const signal = [input.contentSignal, input.title, input.keyword].filter(Boolean).join(" ");
  const inferredType = input.forceSchemaType || inferSchemaTypeFromSignal(signal, input.briefType);

  // Build the prompt. The LLMO variant gets a different system
  // message + schema-type preference list because LLMO readers care
  // about citation surfaces (SpeakableSpec, author, datePublished,
  // mainEntity hierarchy) while SEO readers care about Google rich
  // results (BlogPosting, Article with full article metadata).
  const briefFlavour = input.briefType === "llmo"
    ? "LLMO (AI-search optimised — prioritise structured data that helps citation engines like ChatGPT / Perplexity / Google SGE identify canonical answers). Include SpeakableSpecification when appropriate. Explicit author + datePublished + dateModified critical."
    : "SEO (Google rich-results optimised — prioritise fields that unlock SERP enhancements like article thumbnails, FAQ accordions, how-to steps).";

  const prompt = `Generate JSON-LD schema markup for a planned content page. The page HASN'T been published yet — you're scaffolding structured data the user will paste into the page once they write it.

Brief flavour: ${briefFlavour}
Suggested @type (heuristic from brief signal; override only if you see a clearly better fit from SCHEMA_TYPES): ${inferredType}

Business context:
- Name: ${brand.name}
- Industry: ${brand.industry || "(not set)"}
- Domain: ${brand.domain}
${gbpLocation ? `- Physical location: ${[gbpLocation.address, gbpLocation.city, gbpLocation.state, gbpLocation.zip].filter(Boolean).join(", ")}` : ""}
${gbpLocation?.phone ? `- Phone: ${gbpLocation.phone}` : ""}
${gbpLocation?.avgRating ? `- Rating: ${gbpLocation.avgRating} (${gbpLocation.totalReviews} reviews)` : ""}

Brief context:
- Title: ${input.title || "(not set)"}
- Description: ${input.description || "(not set)"}
- Target keyword: ${input.keyword || "(not set)"}
- Secondary keywords: ${input.secondaryKeywords?.join(", ") || "(none)"}
- Mode: ${input.mode ?? "standard"}

Return ONLY JSON with this exact shape:
{"schemaType":"<chosen @type>","jsonLd":{"@context":"https://schema.org","@type":"<type>", ...populated fields...},"reasoning":"one-sentence reasoning about why the populated fields were chosen","typeReasoning":"one-sentence reasoning about why this @type was selected over the alternatives"}

Use REAL business data where available. Every schema must have @context and @type. Do not invent facts — if the GBP data doesn't include a field, omit the corresponding schema property.`;

  const result = await callLlm(
    [
      { role: "system", content: `You are a JSON-LD structured-data expert. Respond ONLY with valid JSON. ${input.briefType === "llmo" ? "Bias toward AI-search-friendly schema: Article / BlogPosting / MedicalWebPage / FAQPage with full speakable / author / datePublished coverage when applicable." : "Bias toward Google-rich-results friendly schema."}` },
      { role: "user", content: prompt },
    ],
    () => JSON.stringify({
      schemaType: inferredType,
      jsonLd: {
        "@context": "https://schema.org",
        "@type": inferredType,
        name: input.title || brand.name,
        description: input.description,
        keywords: [input.keyword, ...(input.secondaryKeywords ?? [])].filter(Boolean).join(", "),
        author: { "@type": "Organization", name: brand.name, url: `https://${brand.domain}` },
        datePublished: new Date().toISOString().split("T")[0],
      },
      reasoning: "Mock response — configure ANTHROPIC_API_KEY for real generation.",
      typeReasoning: `Heuristic fallback: ${inferredType} inferred from brief signal.`,
    }),
    { maxTokens: 2000 },
  );

  logAiInteraction({
    brandId,
    feature: "schema_gen",
    userPrompt: prompt,
    aiResponse: result.reply,
    model: result.model,
    brandDomain: brand.domain,
  });

  try {
    const parsed = JSON.parse(extractJson(result.reply)) as BriefSchemaResult;
    // Validate shape — an AI that returned the wrong structure should
    // still produce something usable rather than a crash downstream.
    if (!parsed.schemaType || !parsed.jsonLd) {
      throw new Error("missing fields");
    }
    return parsed;
  } catch {
    // Deterministic fallback mirrors the mock response — if the AI
    // returned malformed JSON we still send something coherent back
    // to the client instead of a 500.
    return {
      schemaType: inferredType,
      jsonLd: {
        "@context": "https://schema.org",
        "@type": inferredType,
        name: input.title || brand.name,
        description: input.description,
      },
      reasoning: "AI returned invalid JSON — using a scaffolded template.",
      typeReasoning: `Heuristic fallback: ${inferredType} selected from brief signal.`,
    };
  }
}

export interface ValidationResult { valid: boolean; errors: string[]; warnings: string[] }

export function validateSchema(jsonLd: unknown): ValidationResult {
  const errors: string[] = [];
  const warnings: string[] = [];

  if (!jsonLd || typeof jsonLd !== "object") {
    return { valid: false, errors: ["Schema must be a valid JSON object"], warnings: [] };
  }
  const obj = jsonLd as Record<string, unknown>;

  if (!obj["@context"]) errors.push("Missing @context — should be \"https://schema.org\"");
  else if (obj["@context"] !== "https://schema.org") warnings.push("@context should be \"https://schema.org\"");
  if (!obj["@type"]) errors.push("Missing @type — every schema needs a type (e.g. LocalBusiness, FAQPage)");

  const t = String(obj["@type"] || "");
  if (t === "LocalBusiness" || t === "MedicalBusiness") {
    if (!obj.name) errors.push(`${t} requires "name" property`);
    if (!obj.address) warnings.push(`${t} should have an "address" property`);
  }
  if (t === "FAQPage") {
    if (!Array.isArray(obj.mainEntity) || obj.mainEntity.length === 0) errors.push("FAQPage requires \"mainEntity\" array with Question items");
  }
  if (t === "HowTo") {
    if (!obj.name) errors.push("HowTo requires \"name\" property");
    if (!Array.isArray(obj.step) || obj.step.length === 0) errors.push("HowTo requires \"step\" array");
  }
  if (t === "Article") {
    if (!obj.headline) warnings.push("Article should have \"headline\"");
    if (!obj.author) warnings.push("Article should have \"author\"");
    if (!obj.datePublished) warnings.push("Article should have \"datePublished\"");
  }
  if (t === "Product") {
    if (!obj.name) errors.push("Product requires \"name\"");
  }
  if (t === "Service") {
    if (!obj.name) errors.push("Service requires \"name\"");
    if (!obj.provider) warnings.push("Service should have \"provider\"");
  }

  return { valid: errors.length === 0, errors, warnings };
}
