/**
 * Rank Tracking — Core rank-check service
 * ----------------------------------------
 * Checks keyword positions and stores them as KeywordRanking rows.
 *
 * Two providers, picked in this order:
 *   1. DataForSEO Live SERP Advanced (preferred) — full top-100 scan in
 *      one call, includes SERP feature detection + competitor positions.
 *      Costs ~$0.002 per keyword. Requires DATAFORSEO_LOGIN +
 *      DATAFORSEO_PASSWORD env vars.
 *   2. Google Custom Search JSON API (fallback) — 3 calls per keyword
 *      to cover positions 1-30. Free up to 100 queries/day, no SERP
 *      features. Requires GOOGLE_API_KEY + GOOGLE_SEARCH_CX.
 *
 * The rank-check cron + manual "Check Now" button both call checkBrand,
 * which fans the chosen provider out across every active TrackedKeyword
 * with a concurrency limit + per-request delay.
 */

import { prisma } from "@/lib/prisma";
import {
  checkKeywordPosition as dataforseoCheck,
  isDataForSeoConfigured,
  recordCost,
} from "@/lib/services/dataforseo";

const GOOGLE_API_KEY = process.env.GOOGLE_API_KEY ?? "";
const GOOGLE_CX = process.env.GOOGLE_SEARCH_CX ?? "";
const RESULTS_PER_PAGE = 10;
const PAGES_TO_CHECK = 3; // positions 1-30
const DELAY_MS = 200;
const MAX_CONCURRENT = 5;

function sleep(ms: number) { return new Promise((r) => setTimeout(r, ms)); }

export interface RankResult {
  position: number | null;
  url: string | null;
  snippet: string | null;
  /** SERP features detected on the result page (DataForSEO only). */
  serpFeatures: string[];
  /** Top organic competitors on the same SERP (DataForSEO only). */
  competitors: Array<{ domain: string; position: number; url: string; title: string }>;
  /** Where this row came from — drives the KeywordRanking.source field. */
  source: "dataforseo" | "google_cse" | "none";
  /** USD cost of this single check (0 for the free Google CSE tier). */
  cost: number;
}

/**
 * Checks a single keyword's rank, preferring DataForSEO and falling
 * back to Google Custom Search. The DataForSEO path also returns SERP
 * feature detection + the top 5 organic competitors in the same call.
 */
export async function checkKeywordRank(
  keyword: string,
  domain: string,
  location: string = "United States",
  device: "desktop" | "mobile" = "desktop",
): Promise<RankResult> {
  // ── Preferred path: DataForSEO Live SERP Advanced ────────────────
  if (isDataForSeoConfigured()) {
    try {
      const res = await dataforseoCheck({ keyword, location, device, domain, depth: 100 });
      if (res) {
        recordCost(res.cost);
        return {
          position: res.position,
          url: res.rankingUrl,
          snippet: res.snippet,
          serpFeatures: res.serpFeatures,
          competitors: res.competitors,
          source: "dataforseo",
          cost: res.cost,
        };
      }
    } catch (err) {
      // DataForSEO can fail for legitimate reasons (rate limit, billing,
      // location not supported). Don't crash the whole brand check —
      // log and fall through to the Google CSE fallback below.
      console.error(`[rank-check] DataForSEO failed for "${keyword}":`, err instanceof Error ? err.message : err);
    }
  }

  // ── Fallback: Google Custom Search JSON API ──────────────────────
  if (!GOOGLE_API_KEY || !GOOGLE_CX) {
    // No provider configured — return null so the caller can still
    // create a ranking row (useful in dev environments).
    return { position: null, url: null, snippet: null, serpFeatures: [], competitors: [], source: "none", cost: 0 };
  }

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

  for (let page = 0; page < PAGES_TO_CHECK; page++) {
    const start = page * RESULTS_PER_PAGE + 1;
    const params = new URLSearchParams({
      key: GOOGLE_API_KEY,
      cx: GOOGLE_CX,
      q: keyword,
      num: String(RESULTS_PER_PAGE),
      start: String(start),
    });

    try {
      const res = await fetch(`https://www.googleapis.com/customsearch/v1?${params}`);
      if (!res.ok) {
        // Quota exhausted or other API error — abort gracefully.
        console.warn(`[rank-check] Google API ${res.status} for "${keyword}" start=${start}`);
        break;
      }
      const data = await res.json() as { items?: Array<{ link?: string; snippet?: string }> };
      const items = data.items ?? [];

      for (let i = 0; i < items.length; i++) {
        const link = (items[i].link || "").toLowerCase();
        if (link.includes(cleanDomain)) {
          return {
            position: start + i,
            url: items[i].link || null,
            snippet: items[i].snippet?.slice(0, 500) || null,
            serpFeatures: [],
            competitors: [],
            source: "google_cse",
            cost: 0,
          };
        }
      }
    } catch (err) {
      console.error(`[rank-check] fetch failed for "${keyword}"`, err);
      break;
    }

    if (page < PAGES_TO_CHECK - 1) await sleep(DELAY_MS);
  }

  // Suppress unused-var warning when device is desktop-only path above.
  void device;

  return { position: null, url: null, snippet: null, serpFeatures: [], competitors: [], source: "google_cse", cost: 0 };
}

/**
 * Run-limited parallel executor: calls fns with at most `limit`
 * concurrent jobs and returns settled results.
 */
async function limitedParallel<T>(fns: Array<() => Promise<T>>, limit: number): Promise<PromiseSettledResult<T>[]> {
  const results: PromiseSettledResult<T>[] = new Array(fns.length);
  let idx = 0;

  async function worker() {
    while (idx < fns.length) {
      const i = idx++;
      try { results[i] = { status: "fulfilled", value: await fns[i]() }; }
      catch (reason) { results[i] = { status: "rejected", reason }; }
    }
  }

  await Promise.all(Array.from({ length: Math.min(limit, fns.length) }, () => worker()));
  return results;
}

export interface BrandCheckSummary {
  checked: number;
  improved: number;
  declined: number;
  newRankings: number;
  notFound: number;
  /** Total USD cost across all DataForSEO calls in this run. */
  costUsd: number;
  /** Which provider actually served the checks: dataforseo|google_cse|none|mixed. */
  provider: "dataforseo" | "google_cse" | "none" | "mixed";
}

/**
 * Check all active TrackedKeywords for a brand. Rate-limited to
 * MAX_CONCURRENT parallel requests. Creates a KeywordRanking row per
 * keyword and computes a summary.
 */
export async function checkBrand(brandId: string): Promise<BrandCheckSummary> {
  const brand = await prisma.brand.findUnique({ where: { id: brandId }, select: { domain: true } });
  if (!brand) throw new Error("Brand not found");

  const keywords = await prisma.trackedKeyword.findMany({
    where: { brandId, isActive: true },
    include: { rankings: { orderBy: { checkedAt: "desc" }, take: 1 } },
  });

  const summary: BrandCheckSummary = {
    checked: 0, improved: 0, declined: 0, newRankings: 0, notFound: 0,
    costUsd: 0, provider: "none",
  };
  const providersSeen = new Set<string>();

  const tasks = keywords.map((kw) => async () => {
    const lastRanking = kw.rankings[0] ?? null;
    const device = (kw.device === "mobile" ? "mobile" : "desktop") as "desktop" | "mobile";
    const result = await checkKeywordRank(kw.keyword, brand.domain, kw.location, device);

    await prisma.keywordRanking.create({
      data: {
        trackedKeywordId: kw.id,
        position: result.position,
        previousPosition: lastRanking?.position ?? null,
        url: result.url,
        snippet: result.snippet,
        serpFeatures: result.serpFeatures,
        // Prisma's Json field is unknown-typed; cast through unknown so
        // the array of objects survives serialization without `any`.
        competitors: result.competitors as unknown as object,
        source: result.source === "none" ? "dataforseo" : result.source,
      },
    });

    summary.checked++;
    summary.costUsd += result.cost;
    if (result.source !== "none") providersSeen.add(result.source);

    if (result.position === null) {
      summary.notFound++;
    } else if (lastRanking?.position == null) {
      summary.newRankings++;
    } else if (result.position < lastRanking.position) {
      summary.improved++;
    } else if (result.position > lastRanking.position) {
      summary.declined++;
    }
  });

  await limitedParallel(tasks, MAX_CONCURRENT);

  summary.provider = providersSeen.size === 0 ? "none"
    : providersSeen.size > 1 ? "mixed"
    : (Array.from(providersSeen)[0] as "dataforseo" | "google_cse");

  // Round to four decimals so the JSON response stays readable. Logged
  // independently below for ops visibility.
  summary.costUsd = Math.round(summary.costUsd * 10000) / 10000;
  if (summary.costUsd > 0) {
    console.info(`[rank-check] brand=${brandId} provider=${summary.provider} keywords=${summary.checked} cost=$${summary.costUsd.toFixed(4)}`);
  }

  return summary;
}
