/**
 * DataForSEO API client
 * ─────────────────────
 * One module that wraps every DataForSEO endpoint we hit. Built around
 * three call sites:
 *
 *   1. Rank Tracker — Live SERP positions + SERP feature detection
 *      (checkKeywordPosition). One call per keyword per check; ~$0.002
 *      each via the Live SERP Advanced endpoint.
 *
 *   2. Keyword Research — Search volume + CPC + competition
 *      (getKeywordVolume / getKeywordSuggestions). Cheap; the volume
 *      endpoint accepts up to 1000 keywords per request for ~$0.075
 *      total.
 *
 *   3. Cron — Daily automated rank checks across every brand's tracked
 *      keywords. Same SERP call as (1) but fan-out via the rank-check
 *      service.
 *
 * Auth is via Basic auth using DATAFORSEO_LOGIN + DATAFORSEO_PASSWORD
 * env vars. The `password` is the API password from the DataForSEO API
 * Access page — NOT the dashboard login password.
 *
 * All public functions return null / empty arrays on misconfiguration
 * (missing creds) so callers can gracefully degrade — the rank-check
 * service falls back to Google Custom Search, and the keyword research
 * service falls back to AI estimates.
 */

const DATAFORSEO_BASE = "https://api.dataforseo.com/v3";
const DATAFORSEO_LOGIN = process.env.DATAFORSEO_LOGIN ?? "";
const DATAFORSEO_PASSWORD = process.env.DATAFORSEO_PASSWORD ?? "";

/** Returns true when both env vars are present. Use as a guard before
 *  every API call so dev environments without credentials don't crash. */
export function isDataForSeoConfigured(): boolean {
  return !!(DATAFORSEO_LOGIN && DATAFORSEO_PASSWORD);
}

function getAuthHeader(): string | null {
  if (!isDataForSeoConfigured()) return null;
  return "Basic " + Buffer.from(`${DATAFORSEO_LOGIN}:${DATAFORSEO_PASSWORD}`).toString("base64");
}

/**
 * Low-level POST. All DataForSEO endpoints accept POST with a JSON
 * array as the body (one task per array entry). Returns the parsed
 * JSON. Throws on HTTP errors and on the API's own status_code != 20000
 * envelope. Callers higher up wrap errors and degrade gracefully.
 */
async function dataforseoRequest<T = unknown>(endpoint: string, body: unknown[]): Promise<T> {
  const auth = getAuthHeader();
  if (!auth) throw new Error("DataForSEO credentials not configured");

  const res = await fetch(`${DATAFORSEO_BASE}${endpoint}`, {
    method: "POST",
    headers: { Authorization: auth, "Content-Type": "application/json" },
    body: JSON.stringify(body),
  });

  if (!res.ok) {
    throw new Error(`DataForSEO HTTP ${res.status} on ${endpoint}`);
  }

  const data = (await res.json()) as { status_code?: number; status_message?: string };
  // 20000 is DataForSEO's "Ok" code. Anything else is an envelope-level
  // failure (auth, billing, malformed task) — partial successes still
  // come back as 20000 with per-task statuses inside.
  if (data.status_code !== 20000) {
    console.error("[dataforseo] envelope error:", data.status_code, data.status_message);
    throw new Error(`DataForSEO ${data.status_code}: ${data.status_message ?? "unknown"}`);
  }
  return data as T;
}

// ─── 1. SERP position check (Rank Tracker) ──────────────────────────────────

/** Maps the noisy DataForSEO SERP item types to friendly badge keys
 *  surfaced in the keyword-table UI. Anything not in this map is
 *  ignored — no need to render every esoteric SERP feature. */
const SERP_FEATURE_MAP: Record<string, string> = {
  local_pack: "local_pack",
  featured_snippet: "featured_snippet",
  video: "video",
  images: "images",
  people_also_ask: "people_also_ask",
  knowledge_graph: "knowledge_graph",
  reviews: "reviews",
  shopping: "shopping",
  twitter: "twitter",
  top_stories: "top_stories",
};

export interface SerpItem {
  type: string;
  rank_absolute?: number;
  rank_group?: number;
  domain?: string;
  url?: string;
  title?: string;
  description?: string;
}

export interface CheckKeywordResult {
  /** The brand's organic position 1-100, or null if not in top 100. */
  position: number | null;
  /** The exact URL on the brand's domain that's ranking. */
  rankingUrl: string | null;
  /** The SERP snippet for the brand's ranking page. */
  snippet: string | null;
  /** Friendly SERP feature keys (deduplicated): ["local_pack","reviews"] etc. */
  serpFeatures: string[];
  /** Top 5 organic competitors on the same SERP (excluding the brand). */
  competitors: Array<{ domain: string; position: number; url: string; title: string }>;
  /** Total search results count Google reports for the query. */
  totalResults: number;
  /** Estimated cost for this single call. Logged for monthly cost tracking. */
  cost: number;
}

export interface CheckKeywordParams {
  keyword: string;
  /** "United States" or a city-level location string DataForSEO accepts. */
  location: string;
  /** "English" or another full language name DataForSEO accepts. */
  language?: string;
  device?: "desktop" | "mobile";
  /** Brand domain — used to find the brand's ranking row in the SERP items. */
  domain: string;
  /** How deep to scan. 100 = full page 1-10. Lower = cheaper. */
  depth?: number;
}

/**
 * Live SERP check for a single keyword. Returns the brand's organic
 * position, the SERP features detected on the page, and the top 5
 * competitors in one call. Cost: ~$0.002 per call (Live SERP Advanced).
 *
 * Returns null when DataForSEO isn't configured so callers can fall
 * back to Google Custom Search or skip the check entirely.
 */
export async function checkKeywordPosition(
  params: CheckKeywordParams,
): Promise<CheckKeywordResult | null> {
  if (!isDataForSeoConfigured()) return null;

  const cleanDomain = params.domain
    .replace(/^https?:\/\/(www\.)?/, "")
    .replace(/\/$/, "")
    .toLowerCase();

  type ResponseShape = {
    cost?: number;
    tasks?: Array<{
      cost?: number;
      result?: Array<{
        items?: SerpItem[];
        se_results_count?: number;
      }>;
    }>;
  };

  const data = await dataforseoRequest<ResponseShape>("/serp/google/organic/live/advanced", [{
    keyword: params.keyword,
    location_name: params.location || "United States",
    language_name: params.language || "English",
    device: params.device || "desktop",
    depth: params.depth ?? 100,
  }]);

  const task = data.tasks?.[0];
  const items = task?.result?.[0]?.items ?? [];

  // Walk the items in a single pass: collect SERP features, locate the
  // brand's organic row, and build the competitor list. One loop > three.
  const serpFeatureSet = new Set<string>();
  let position: number | null = null;
  let rankingUrl: string | null = null;
  let snippet: string | null = null;
  const organicCompetitors: Array<{ domain: string; position: number; url: string; title: string }> = [];

  for (const item of items) {
    if (item.type && SERP_FEATURE_MAP[item.type]) {
      serpFeatureSet.add(SERP_FEATURE_MAP[item.type]);
    }
    if (item.type === "organic" && item.domain) {
      const domainLower = item.domain.toLowerCase();
      if (domainLower.includes(cleanDomain) && position === null) {
        position = item.rank_absolute ?? item.rank_group ?? null;
        rankingUrl = item.url ?? null;
        snippet = item.description?.slice(0, 500) ?? null;
      } else if (!domainLower.includes(cleanDomain) && organicCompetitors.length < 5) {
        organicCompetitors.push({
          domain: item.domain,
          position: item.rank_absolute ?? item.rank_group ?? 0,
          url: item.url ?? "",
          title: (item.title ?? "").slice(0, 200),
        });
      }
    }
  }

  return {
    position,
    rankingUrl,
    snippet,
    serpFeatures: Array.from(serpFeatureSet),
    competitors: organicCompetitors,
    totalResults: task?.result?.[0]?.se_results_count ?? 0,
    // DataForSEO returns per-task cost in USD. Falls back to the
    // documented Live SERP Advanced rate when missing.
    cost: task?.cost ?? data.cost ?? 0.002,
  };
}

// ─── 2. Search volume + CPC (Keyword Research) ──────────────────────────────

export type KeywordIntent = "informational" | "navigational" | "commercial" | "transactional" | "mixed" | "unknown";

export interface KeywordVolumeData {
  keyword: string;
  volume: number | null;
  cpc: number | null;
  competition: number | null;
  competitionLevel: "LOW" | "MEDIUM" | "HIGH" | null;
  monthlySearches: Array<{ year: number; month: number; search_volume: number }>;
  intent: KeywordIntent;
  foreignIntents: string[];
}

/**
 * Batch volume lookup. DataForSEO accepts up to 1000 keywords per
 * request — we slice defensively. Returns an empty array when not
 * configured so callers can fall back without exception handling.
 */
export async function getKeywordVolume(
  keywords: string[],
  location: string = "United States",
  language: string = "English",
): Promise<KeywordVolumeData[]> {
  if (!isDataForSeoConfigured() || keywords.length === 0) return [];

  type ResponseShape = {
    tasks?: Array<{
      result?: Array<{
        keyword?: string;
        search_volume?: number | null;
        cpc?: number | null;
        competition?: number | null;
        competition_level?: "LOW" | "MEDIUM" | "HIGH" | null;
        monthly_searches?: Array<{ year: number; month: number; search_volume: number }>;
      }>;
    }>;
  };

  const data = await dataforseoRequest<ResponseShape>("/keywords_data/google_ads/search_volume/live", [{
    keywords: keywords.slice(0, 1000),
    location_name: location,
    language_name: language,
  }]);

  const result = data.tasks?.[0]?.result ?? [];
  return result.map((r) => ({
    keyword: r.keyword ?? "",
    volume: r.search_volume ?? null,
    cpc: r.cpc ?? null,
    competition: r.competition ?? null,
    competitionLevel: r.competition_level ?? null,
    monthlySearches: r.monthly_searches ?? [],
    intent: "unknown",
    foreignIntents: [],
  }));
}

// ─── 2b. Keyword intent enrichment (DataForSEO Labs) ─────────────────────────

/** Fetch search intent for a batch of keywords using the DataForSEO Labs
 *  keyword suggestions endpoint. Returns a Map of keyword → intent data.
 *  Used to enrich volume results from the Google Ads endpoint. */
export async function getKeywordIntents(
  keywords: string[],
  location: string = "United States",
  language: string = "English",
): Promise<Map<string, { intent: KeywordIntent; foreignIntents: string[] }>> {
  const result = new Map<string, { intent: KeywordIntent; foreignIntents: string[] }>();
  if (!isDataForSeoConfigured() || keywords.length === 0) return result;

  try {
    type LabsResponse = {
      tasks?: Array<{
        result?: Array<{
          items?: Array<{
            keyword?: string;
            keyword_data?: { keyword?: string };
            search_intent_info?: {
              main_intent?: string;
              foreign_intent?: string[];
            };
          }>;
        }>;
      }>;
    };

    const data = await dataforseoRequest<LabsResponse>("/dataforseo_labs/google/keyword_suggestions/live", [{
      keywords: keywords.slice(0, 20),
      location_name: location,
      language_name: language,
      include_seed_keyword: true,
      limit: keywords.length,
    }]);

    const items = data.tasks?.[0]?.result?.[0]?.items ?? [];
    for (const item of items) {
      const kw = (item.keyword || item.keyword_data?.keyword || "").toLowerCase();
      if (!kw) continue;
      const si = item.search_intent_info;
      if (!si?.main_intent) continue;
      const main = si.main_intent.toLowerCase();
      const foreign = (si.foreign_intent ?? []).map((f) => f.toLowerCase());
      const intent: KeywordIntent = (["informational", "navigational", "commercial", "transactional"].includes(main)
        ? main
        : foreign.length > 0 ? "mixed" : "unknown") as KeywordIntent;
      result.set(kw, { intent, foreignIntents: foreign });
    }
  } catch (err) {
    console.warn("[dataforseo] intent enrichment failed:", err instanceof Error ? err.message : err);
  }

  return result;
}

// ─── 3. Domain Intersection (shared keywords with competitor) ───────────────

export interface DomainIntersectionRow {
  keyword: string;
  volume: number | null;
  cpc: number | null;
  difficulty: number | null;
  brandPosition: number | null;
  brandUrl: string | null;
  competitorPosition: number | null;
  competitorUrl: string | null;
  /** "winning" = brand outranks competitor; "losing" = competitor outranks
   *  brand; "brand_only" = only brand ranks; "competitor_only" = only
   *  competitor ranks (i.e., a content gap). */
  status: "winning" | "losing" | "brand_only" | "competitor_only" | "unknown";
  serpFeatures: string[];
}

export interface DomainIntersectionResult {
  rows: DomainIntersectionRow[];
  totalCount: number;
  costUsd: number;
}

/**
 * Domain Intersection — keywords that BOTH domains rank for in the
 * top 100. The richest single call for competitive analysis: returns
 * keyword volume, CPC, difficulty, AND both domains' positions and
 * landing URLs in one request. Cost: ~$0.02.
 *
 * Returns null when DataForSEO isn't configured. Throws on hard
 * envelope failures so callers can decide whether to surface to user.
 *
 * Pass intersections=false to get content-gap data (keywords competitor
 * ranks for that brand doesn't) — DataForSEO returns rows where one of
 * the two SERP elements is null in that case.
 */
export async function getDomainIntersection(
  brandDomain: string,
  competitorDomain: string,
  location: string = "United States",
  language: string = "English",
  limit: number = 200,
): Promise<DomainIntersectionResult | null> {
  if (!isDataForSeoConfigured()) return null;

  type SerpItem = { rank_group?: number; relative_url?: string };
  type Row = {
    keyword_data?: {
      keyword?: string;
      keyword_info?: { search_volume?: number | null; cpc?: number | null };
      keyword_properties?: { keyword_difficulty?: number | null };
      serp_info?: { serp_item_types?: string[] };
    };
    first_domain_serp_element?: { serp_item?: SerpItem };
    second_domain_serp_element?: { serp_item?: SerpItem };
  };
  type ResponseShape = {
    cost?: number;
    tasks?: Array<{
      cost?: number;
      result?: Array<{ items?: Row[]; total_count?: number }>;
    }>;
  };

  const data = await dataforseoRequest<ResponseShape>(
    "/dataforseo_labs/google/domain_intersection/live",
    [{
      target1: stripDomain(brandDomain),
      target2: stripDomain(competitorDomain),
      location_name: location,
      language_name: language,
      limit,
      // Sort by brand position ascending so the most threatened (brand
      // not ranking → comes last as null) and best (brand at #1 → first)
      // appear in obvious order.
      order_by: ["first_domain_serp_element.serp_item.rank_group,asc"],
    }],
  );

  const task = data.tasks?.[0];
  const items = task?.result?.[0]?.items ?? [];
  const cost = task?.cost ?? data.cost ?? 0.02;

  const rows: DomainIntersectionRow[] = items.map((item) => {
    const brandPos = item.first_domain_serp_element?.serp_item?.rank_group ?? null;
    const compPos = item.second_domain_serp_element?.serp_item?.rank_group ?? null;
    let status: DomainIntersectionRow["status"] = "unknown";
    if (brandPos != null && compPos != null) status = brandPos < compPos ? "winning" : "losing";
    else if (brandPos != null) status = "brand_only";
    else if (compPos != null) status = "competitor_only";
    return {
      keyword: item.keyword_data?.keyword ?? "",
      volume: item.keyword_data?.keyword_info?.search_volume ?? null,
      cpc: item.keyword_data?.keyword_info?.cpc ?? null,
      difficulty: item.keyword_data?.keyword_properties?.keyword_difficulty ?? null,
      brandPosition: brandPos,
      brandUrl: item.first_domain_serp_element?.serp_item?.relative_url ?? null,
      competitorPosition: compPos,
      competitorUrl: item.second_domain_serp_element?.serp_item?.relative_url ?? null,
      status,
      serpFeatures: item.keyword_data?.serp_info?.serp_item_types ?? [],
    };
  });

  return {
    rows,
    totalCount: task?.result?.[0]?.total_count ?? rows.length,
    costUsd: cost,
  };
}

// ─── 4. Ranked Keywords (per-domain) ────────────────────────────────────────

export interface RankedKeywordRow {
  keyword: string;
  position: number | null;
  url: string | null;
  volume: number | null;
  difficulty: number | null;
  cpc: number | null;
}

export interface RankedKeywordsResult {
  keywords: RankedKeywordRow[];
  totalCount: number;
  costUsd: number;
}

/**
 * Ranked Keywords for a single domain — what does THIS domain rank for?
 * Used to fill the "Top Keywords (N ranking)" panel inside each
 * CompetitorCard. Cost: ~$0.05 per call. Fetched on user click only —
 * never preloaded — to keep the bill predictable.
 */
export async function getRankedKeywords(
  domain: string,
  location: string = "United States",
  language: string = "English",
  limit: number = 50,
  maxPosition: number = 20,
): Promise<RankedKeywordsResult | null> {
  if (!isDataForSeoConfigured()) return null;

  type Row = {
    keyword_data?: {
      keyword?: string;
      keyword_info?: { search_volume?: number | null; cpc?: number | null };
      keyword_properties?: { keyword_difficulty?: number | null };
    };
    ranked_serp_element?: {
      serp_item?: { rank_group?: number; relative_url?: string };
    };
  };
  type ResponseShape = {
    cost?: number;
    tasks?: Array<{
      cost?: number;
      result?: Array<{ items?: Row[]; total_count?: number }>;
    }>;
  };

  const data = await dataforseoRequest<ResponseShape>(
    "/dataforseo_labs/google/ranked_keywords/live",
    [{
      target: stripDomain(domain),
      location_name: location,
      language_name: language,
      limit,
      order_by: ["keyword_data.keyword_info.search_volume,desc"],
      filters: [["ranked_serp_element.serp_item.rank_group", "<=", maxPosition]],
    }],
  );

  const task = data.tasks?.[0];
  const items = task?.result?.[0]?.items ?? [];
  const cost = task?.cost ?? data.cost ?? 0.05;

  return {
    keywords: items.map((item) => ({
      keyword: item.keyword_data?.keyword ?? "",
      position: item.ranked_serp_element?.serp_item?.rank_group ?? null,
      url: item.ranked_serp_element?.serp_item?.relative_url ?? null,
      volume: item.keyword_data?.keyword_info?.search_volume ?? null,
      difficulty: item.keyword_data?.keyword_properties?.keyword_difficulty ?? null,
      cpc: item.keyword_data?.keyword_info?.cpc ?? null,
    })),
    totalCount: task?.result?.[0]?.total_count ?? items.length,
    costUsd: cost,
  };
}

/** Strip protocol + leading www. + trailing slash. DataForSEO accepts
 *  bare hostnames in the `target` field — leading https:// causes
 *  silent zero-result responses. */
function stripDomain(domain: string): string {
  return domain.replace(/^https?:\/\//, "").replace(/^www\./, "").replace(/\/.*$/, "").toLowerCase();
}

// ─── 5. Cost tracking (best-effort, log-only) ───────────────────────────────

let costAccumulator = 0;
let costAccumulatorWindowStart = Date.now();

/** Track per-process cost. Resets every 24h. Used by the daily cron to
 *  enforce a soft cap and surface monthly burn in the logs. */
export function recordCost(usd: number): { totalUsd: number; windowStartedAt: Date } {
  // Reset the rolling window every 24h so we don't double-count across
  // serverless cold starts hitting the same module instance.
  if (Date.now() - costAccumulatorWindowStart > 86_400_000) {
    costAccumulator = 0;
    costAccumulatorWindowStart = Date.now();
  }
  costAccumulator += usd;
  return { totalUsd: costAccumulator, windowStartedAt: new Date(costAccumulatorWindowStart) };
}

/** Read the running cost without incrementing. */
export function getCostSinceWindowStart(): { totalUsd: number; windowStartedAt: Date } {
  return { totalUsd: costAccumulator, windowStartedAt: new Date(costAccumulatorWindowStart) };
}
