/**
 * AI-powered competitor discovery
 * ────────────────────────────────
 * Two-stage pipeline:
 *
 *   1. Claude (or fallback LLM) suggests candidate competitors based on
 *      brand profile + top GSC keywords + Site Tag outbound clicks.
 *   2. DataForSEO *softly* validates each candidate. Confirmed overlap
 *      upgrades the competitor's status; missing overlap is noted but
 *      NEVER used to flag the candidate as "not a real competitor".
 *      Small local practices routinely lack enough ranking data in
 *      DataForSEO's corpus to produce an overlap, and we'd rather
 *      trust the AI than discourage the user from tracking real
 *      rivals.
 *
 * For multi-location brands, discoverCompetitors() runs a separate
 * per-location pass (up to 3) and merges results, deduplicating by
 * domain. Each returned candidate carries discoveredForLocations[]
 * so the UI can show where it was found.
 *
 * Cost: stage 1 is one LLM call per location (~$0.01 each). Stage 2
 * is one Domain Intersection call per candidate (~$0.02), plus a
 * Ranked Keywords fallback call (~$0.05) ONLY when intersection
 * returns 0 AND the candidate hasn't already been proved legitimate.
 *
 * Excluded by default: directories and large national publishers that
 * pollute results (yelp, healthgrades, webmd, mayoclinic, wikipedia,
 * facebook, instagram, etc.). These are filtered post-LLM so the
 * model's suggestions stay broad and we strip noise centrally.
 */

import { prisma } from "@/lib/prisma";
import { callLlm } from "@/lib/ai-avatar/llm-client";
import { logAiInteraction } from "@/lib/services/ai-interaction-log";
import { getBrandLocations } from "@/lib/brand-helpers";
import {
  getDomainIntersection,
  getRankedKeywords,
  isDataForSeoConfigured,
  recordCost,
} from "@/lib/services/dataforseo";

export interface DiscoveredCompetitor {
  domain: string;
  name: string;
  /** AI-assigned classification: direct local rival vs national leader. */
  type: "DIRECT" | "NATIONAL" | "ADJACENT" | "UNKNOWN";
  /** AI-assigned overlap label. DataForSEO upgrades it when real
   *  overlap is found; otherwise the AI label stands. Never set to
   *  "NONE" based on missing DataForSEO data alone. */
  overlap: "HIGH" | "MEDIUM" | "LOW";
  /** AI-generated 1-sentence justification. */
  reason: string;
  /** Live keyword overlap count from DataForSEO. null = validation
   *  couldn't run (missing creds, API error, or 0 rows returned — the
   *  UI reads `validation` below to distinguish these cases). */
  validatedOverlapCount: number | null;
  /** Validation status drives the row's visual treatment.
   *   confirmed   — DataForSEO found overlap > 0.
   *   unverified  — DataForSEO returned 0 shared keywords. AI assessment
   *                 stands; typical for small local businesses with
   *                 limited ranking data.
   *   skipped     — DataForSEO not configured / API failed. No extra
   *                 info either way. */
  validation: "confirmed" | "unverified" | "skipped";
  /** Optional short note surfaced below the main reason — "12 shared
   *  keywords" on confirmed, "AI identified — keyword overlap data
   *  pending" on unverified, "120 ranking keywords indexed" when the
   *  Ranked Keywords fallback found data on the competitor domain. */
  validationNote: string;
  /** True when this candidate's domain is already tracked as a competitor. */
  alreadyTracked: boolean;
  /** Locations this competitor was found in during discovery. Null for
   *  manual adds and pre-migration rows. */
  discoveredForLocations?: string[];
}

const EXCLUDED_DOMAINS = [
  // Directories
  "yelp.com", "healthgrades.com", "vitals.com", "ratemds.com", "zocdoc.com",
  "wellness.com", "doctor.webmd.com", "yellowpages.com", "bbb.org",
  "angi.com", "thumbtack.com", "houzz.com", "tripadvisor.com",
  // Encyclopedias / publishers
  "wikipedia.org", "webmd.com", "mayoclinic.org", "clevelandclinic.org",
  "healthline.com", "medlineplus.gov", "nih.gov", "cdc.gov", "verywellhealth.com",
  // Social
  "facebook.com", "instagram.com", "youtube.com", "twitter.com", "x.com",
  "linkedin.com", "tiktok.com", "pinterest.com", "reddit.com",
  // Marketplaces / generic
  "amazon.com", "ebay.com", "google.com",
];

function isExcluded(domain: string): boolean {
  const d = domain.toLowerCase();
  return EXCLUDED_DOMAINS.some((ex) => d === ex || d.endsWith(`.${ex}`));
}

function overlapRank(overlap: DiscoveredCompetitor["overlap"]): number {
  return overlap === "HIGH" ? 3 : overlap === "MEDIUM" ? 2 : 1;
}

// ─── Per-location AI + validation pass ────────────────────────────────────────

interface BrandSnapshot {
  name: string;
  domain: string;
  industry: string | null;
}

async function discoverForLocation(
  brand: BrandSnapshot,
  location: string | null,
  focus: "local" | "national",
  limit: number,
  trackedSet: Set<string>,
  topQueries: Array<{ query: string }>,
  outboundDomains: string[],
  brandId: string,
): Promise<{ candidates: DiscoveredCompetitor[]; costUsd: number }> {
  // ── Stage 1: AI suggestions ─────────────────────────────────────
  const locationFragment = location ? `based in ${location}` : "";
  const prompt = `Identify the top ${limit} ${focus === "local" ? "LOCAL direct" : "NATIONAL"} competitors for ${brand.name} (${brand.domain}) ${locationFragment}, a ${brand.industry || "general"} business.

Top ranking keywords: ${topQueries.length > 0 ? topQueries.map((q) => q.query).join(", ") : "unknown"}
Sites visitors also visit (from Site Tag outbound clicks): ${outboundDomains.length > 0 ? outboundDomains.join(", ") : "unknown"}

Return ONLY a JSON array, no markdown:
[{"domain":"competitor.com","name":"Competitor Name","type":"DIRECT|NATIONAL|ADJACENT","overlap":"HIGH|MEDIUM|LOW","reason":"one sentence why they compete"}]

Rules:
- ${focus === "local" ? "Focus on local competitors first; prioritise businesses in the same city/region." : "Focus on national/large competitors with broad reach."}
- Exclude directories, encyclopedias, social networks, and generic listings (yelp, healthgrades, webmd, mayoclinic, wikipedia, facebook, etc.).
- Use bare hostnames (no https://, no trailing slash, no www.).
- Aim for ${limit} competitors. Fewer is fine if you can't find ${limit} good ones.`;

  const llm = await callLlm(
    [
      { role: "system", content: "You are a competitive intelligence analyst. Respond ONLY with a valid JSON array." },
      { role: "user", content: prompt },
    ],
    () => "[]",
    { maxTokens: 1500 },
  );

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

  // Parse defensively — strip code fences if the model added them.
  let suggestions: Array<{ domain?: string; name?: string; type?: string; overlap?: string; reason?: string }> = [];
  try {
    let s = llm.reply.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);
    const parsed = JSON.parse(s);
    if (Array.isArray(parsed)) suggestions = parsed;
  } catch (err) {
    console.error("[competitor-discovery] LLM parse failed:", err instanceof Error ? err.message : err);
  }

  // De-duplicate by hostname + filter exclusions + the brand's own domain.
  const seen = new Set<string>();
  const candidates: DiscoveredCompetitor[] = [];
  const brandHost = brand.domain.replace(/^https?:\/\//, "").replace(/^www\./, "").toLowerCase();
  for (const s of suggestions) {
    if (!s.domain) continue;
    const domain = s.domain.replace(/^https?:\/\//, "").replace(/^www\./, "").replace(/\/.*$/, "").toLowerCase();
    if (!domain || domain === brandHost || seen.has(domain) || isExcluded(domain)) continue;
    seen.add(domain);
    candidates.push({
      domain,
      name: s.name?.slice(0, 100) ?? domain,
      type: (["DIRECT", "NATIONAL", "ADJACENT"].includes(s.type ?? "") ? s.type : "UNKNOWN") as DiscoveredCompetitor["type"],
      overlap: (["HIGH", "MEDIUM", "LOW"].includes(s.overlap ?? "") ? s.overlap : "LOW") as DiscoveredCompetitor["overlap"],
      reason: s.reason?.slice(0, 280) ?? "Competitor identified by AI.",
      validatedOverlapCount: null,
      validation: "skipped",
      validationNote: "",
      alreadyTracked: trackedSet.has(domain),
    });
  }

  // ── Stage 2: DataForSEO soft validation ──────────────────────────
  let totalCostUsd = 0;
  if (isDataForSeoConfigured() && candidates.length > 0) {
    const validations = await Promise.all(
      candidates.map(async (c) => {
        try {
          const res = await getDomainIntersection(brand.domain, c.domain, "United States", "English", 100);
          if (!res) return { domain: c.domain, count: null, cost: 0 };
          recordCost(res.costUsd);
          const overlapCount = res.rows.filter((r) => r.brandPosition != null && r.competitorPosition != null).length;
          return { domain: c.domain, count: overlapCount, cost: res.costUsd };
        } catch (err) {
          console.error(`[competitor-discovery] intersection failed for ${c.domain}:`, err instanceof Error ? err.message : err);
          return { domain: c.domain, count: null, cost: 0 };
        }
      }),
    );

    const fallbackTargets: string[] = [];
    for (const v of validations) {
      const c = candidates.find((x) => x.domain === v.domain);
      if (!c) continue;
      c.validatedOverlapCount = v.count;
      totalCostUsd += v.cost;
      if (v.count != null && v.count > 0) {
        c.validation = "confirmed";
        c.validationNote = `${v.count} shared keyword${v.count === 1 ? "" : "s"} with you`;
        const bucket = v.count >= 50 ? "HIGH" : v.count >= 15 ? "MEDIUM" : "LOW";
        if (overlapRank(bucket as DiscoveredCompetitor["overlap"]) > overlapRank(c.overlap)) {
          c.overlap = bucket as DiscoveredCompetitor["overlap"];
        }
      } else {
        fallbackTargets.push(c.domain);
      }
    }

    if (fallbackTargets.length > 0 && fallbackTargets.length <= 8) {
      const fallbacks = await Promise.all(
        fallbackTargets.map(async (domain) => {
          try {
            const res = await getRankedKeywords(domain, "United States", "English", 10, 20);
            if (!res) return { domain, total: null, cost: 0 };
            recordCost(res.costUsd);
            return { domain, total: res.totalCount, cost: res.costUsd };
          } catch (err) {
            console.error(`[competitor-discovery] ranked-keywords fallback failed for ${domain}:`, err instanceof Error ? err.message : err);
            return { domain, total: null, cost: 0 };
          }
        }),
      );

      for (const f of fallbacks) {
        const c = candidates.find((x) => x.domain === f.domain);
        if (!c) continue;
        totalCostUsd += f.cost;
        if (f.total != null && f.total > 0) {
          c.validation = "unverified";
          c.validationNote = `${f.total.toLocaleString()} ranking keyword${f.total === 1 ? "" : "s"} indexed — keyword overlap data pending`;
        } else {
          c.validation = "unverified";
          c.validationNote = "Limited ranking data — AI assessment stands";
        }
      }
    } else {
      for (const domain of fallbackTargets) {
        const c = candidates.find((x) => x.domain === domain);
        if (!c) continue;
        c.validation = "unverified";
        c.validationNote = "Keyword overlap data unavailable — AI assessment stands";
      }
    }
  }

  return { candidates, costUsd: totalCostUsd };
}

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

export async function discoverCompetitors(
  brandId: string,
  options?: {
    focus?: "local" | "national";
    limit?: number;
    locations?: string[];
  },
): Promise<{ candidates: DiscoveredCompetitor[]; costUsd: number; locationsRun: string[] }> {
  const focus = options?.focus ?? "local";
  const limit = Math.min(options?.limit ?? 10, 15);

  // ── Gather brand context ────────────────────────────────────────
  const brand = await prisma.brand.findUnique({
    where: { id: brandId },
    select: { name: true, domain: true, industry: true, location: true, locations: true },
  });
  if (!brand) throw new Error("Brand not found");

  // National discovery never uses locations — skip resolution entirely
  // so a brand with 14 locations doesn't hit the 3-location cap.
  // Local discovery resolves from caller-supplied list or brand array.
  let passLocations: (string | null)[];
  let locationsResolved: string[];

  if (focus === "national") {
    passLocations = [null];
    locationsResolved = [];
  } else {
    const resolvedLocations = options?.locations?.length
      ? options.locations
      : getBrandLocations(brand);

    if (resolvedLocations.length > 3) {
      throw new Error("Maximum 3 locations per synchronous discovery run");
    }
    if (resolvedLocations.length === 0) {
      throw new Error("Brand has no locations configured. Add a location to use local discovery.");
    }
    passLocations = resolvedLocations;
    locationsResolved = resolvedLocations;
  }

  // Top 20 GSC queries — shared across all location passes.
  const topQueries = await prisma.gscQuery.findMany({
    where: { brandId },
    orderBy: { clicks: "desc" },
    take: 20,
    select: { query: true },
  });

  // Already-tracked competitor domains.
  const tracked = await prisma.competitor.findMany({
    where: { brandId, isActive: true },
    select: { domain: true },
  });
  const trackedSet = new Set(tracked.map((c) => c.domain.toLowerCase()));

  // Site Tag outbound-click destinations — shared across all passes.
  let outboundDomains: string[] = [];
  try {
    const site = await prisma.trackedSite.findUnique({ where: { brandId }, select: { id: true } });
    if (site) {
      const since = new Date(Date.now() - 30 * 86_400_000);
      const events = await prisma.trackedEvent.findMany({
        where: { trackedSiteId: site.id, eventType: "outbound_click", timestamp: { gte: since } },
        select: { metadata: true },
        take: 500,
      });
      const counts = new Map<string, number>();
      for (const ev of events) {
        const ed = ev.metadata as Record<string, unknown> | null;
        const href = (ed?.href as string) ?? (ed?.destinationUrl as string) ?? (ed?.url as string) ?? "";
        try {
          const host = new URL(href).hostname.replace(/^www\./, "");
          if (host && !host.endsWith(brand.domain.replace(/^https?:\/\//, "").replace(/^www\./, "")) && !isExcluded(host)) {
            counts.set(host, (counts.get(host) ?? 0) + 1);
          }
        } catch { /* malformed URL — skip */ }
      }
      outboundDomains = [...counts.entries()].sort(([, a], [, b]) => b - a).slice(0, 10).map(([d]) => d);
    }
  } catch { /* no site tag → no outbound data */ }

  // ── Sequential per-location passes with dedup ───────────────────
  const allCandidatesMap = new Map<string, DiscoveredCompetitor>();
  let totalCostUsd = 0;

  for (const loc of passLocations) {
    const result = await discoverForLocation(
      brand, loc, focus, limit, trackedSet, topQueries, outboundDomains, brandId,
    );
    totalCostUsd += result.costUsd;
    for (const c of result.candidates) {
      const existing = allCandidatesMap.get(c.domain);
      if (!existing) {
        allCandidatesMap.set(c.domain, {
          ...c,
          discoveredForLocations: loc ? [loc] : [],
        });
      } else {
        if (loc) {
          existing.discoveredForLocations = [...(existing.discoveredForLocations ?? []), loc];
        }
        // Keep highest overlap bucket across passes
        if (overlapRank(c.overlap) > overlapRank(existing.overlap)) {
          existing.overlap = c.overlap;
          existing.validatedOverlapCount = c.validatedOverlapCount;
          existing.validation = c.validation;
          existing.validationNote = c.validationNote;
        }
      }
    }
  }

  const candidates = Array.from(allCandidatesMap.values());

  // Sort: tracked last, then confirmed first, then by overlap bucket
  // (HIGH > MEDIUM > LOW), then by validated count desc.
  const validationRank: Record<DiscoveredCompetitor["validation"], number> = { confirmed: 2, unverified: 1, skipped: 0 };
  candidates.sort((a, b) => {
    if (a.alreadyTracked !== b.alreadyTracked) return a.alreadyTracked ? 1 : -1;
    if (a.validation !== b.validation) return validationRank[b.validation] - validationRank[a.validation];
    if (a.overlap !== b.overlap) return overlapRank(b.overlap) - overlapRank(a.overlap);
    return (b.validatedOverlapCount ?? 0) - (a.validatedOverlapCount ?? 0);
  });

  const locationsRun = locationsResolved;
  console.info(`[competitor-discovery] brand=${brandId} focus=${focus} locations=${locationsRun.length || 1} candidates=${candidates.length} cost=$${totalCostUsd.toFixed(4)}`);
  return { candidates, costUsd: Math.round(totalCostUsd * 10000) / 10000, locationsRun };
}
