/**
 * GET /auth/google/callback — Google OAuth callback
 * ---------------------------------------------------
 * Route handler (not a page) at a non-/api/ path so Clerk
 * middleware doesn't intercept it. This is the active callback —
 * /api/auth/google/route.ts redirects Google here. (The copy at
 * /api/auth/google/callback/route.ts mirrors this logic for
 * environments where the /api/ prefix is needed; keep the two in
 * sync when editing either.)
 *
 * On success:
 *   1. Validates the brand in `state` still exists — prevents FK
 *      failures on the upsert below when a brand was deleted mid-flow
 *      or when a state token was crafted against a different env.
 *   2. Exchanges the authorization code for tokens.
 *   3. Upserts the BrandIntegration row + stores encrypted tokens.
 *   4. Auto-matches a GSC site / GA4 property against the brand's
 *      domain and pre-saves it so the Integrations page doesn't
 *      land on a "No site URL configured" broken state.
 *   5. Fires a backfill sync only when a property was auto-matched
 *      (otherwise the picker handles first-sync after selection).
 *
 * Errors always redirect with an opaque `error=<code>` param — we
 * never serialize raw Prisma / Google error strings into the URL
 * because they leak implementation details (SQL fragments, table
 * names) to the browser. The full error stack is logged via
 * console.error so it surfaces in Vercel Functions.
 */

import { NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import { saveCredentials, getCredentials } from "@/lib/services/integration-credentials";
import { syncIntegration } from "@/lib/integrations";
import { log } from "@/lib/logger";
import { APP_URL } from "@/lib/config";

export const dynamic = "force-dynamic";

const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID || "";
const GOOGLE_CLIENT_SECRET = process.env.GOOGLE_CLIENT_SECRET || "";

// ─── Auto-match helpers ─────────────────────────────────────────────────

/** Normalise a domain for comparison: strip scheme, www., trailing slash,
 *  lowercase. So "https://www.Example.com/" and "example.com" match. */
function normaliseDomain(raw: string): string {
  return raw
    .toLowerCase()
    .replace(/^sc-domain:/, "")
    .replace(/^https?:\/\//, "")
    .replace(/^www\./, "")
    .replace(/\/+$/, "")
    .trim();
}

async function findMatchingGscSite(accessToken: string, brandDomain: string): Promise<string | null> {
  try {
    const res = await fetch("https://www.googleapis.com/webmasters/v3/sites", {
      headers: { Authorization: `Bearer ${accessToken}` },
      signal: AbortSignal.timeout(8000),
    });
    if (!res.ok) return null;
    const data = (await res.json()) as { siteEntry?: Array<{ siteUrl: string; permissionLevel?: string }> };
    const target = normaliseDomain(brandDomain);
    const matches = (data.siteEntry ?? [])
      .filter((s) => normaliseDomain(s.siteUrl) === target)
      .sort((a, b) => {
        const score = (p?: string) => (p === "siteOwner" ? 2 : p === "siteFullUser" ? 1 : 0);
        return score(b.permissionLevel) - score(a.permissionLevel);
      });
    return matches.length > 0 ? matches[0].siteUrl : null;
  } catch {
    return null;
  }
}

async function findMatchingGa4Property(
  accessToken: string,
  brandDomain: string,
  brandName: string,
): Promise<{ propertyId: string; propertyName: string } | null> {
  try {
    const res = await fetch(
      "https://analyticsadmin.googleapis.com/v1beta/accountSummaries",
      { headers: { Authorization: `Bearer ${accessToken}` }, signal: AbortSignal.timeout(8000) },
    );
    if (!res.ok) return null;
    const data = (await res.json()) as {
      accountSummaries?: Array<{
        displayName?: string;
        propertySummaries?: Array<{ property: string; displayName: string }>;
      }>;
    };
    const target = normaliseDomain(brandDomain);
    const nameLc = brandName.toLowerCase();
    const allProps = (data.accountSummaries ?? []).flatMap((acc) =>
      (acc.propertySummaries ?? []).map((p) => ({
        propertyId: p.property.replace("properties/", ""),
        propertyName: p.displayName,
      })),
    );
    const domainMatches = allProps.filter((p) => normaliseDomain(p.propertyName).includes(target));
    if (domainMatches.length === 1) return { propertyId: domainMatches[0].propertyId, propertyName: domainMatches[0].propertyName };
    if (domainMatches.length > 1) return null;
    const nameMatches = allProps.filter((p) => p.propertyName.toLowerCase().includes(nameLc));
    if (nameMatches.length === 1) return { propertyId: nameMatches[0].propertyId, propertyName: nameMatches[0].propertyName };
    return null;
  } catch {
    return null;
  }
}

// ─── Handler ────────────────────────────────────────────────────────────

export async function GET(req: Request) {
  const appUrl = APP_URL;
  const { searchParams } = new URL(req.url);

  const code = searchParams.get("code");
  const stateParam = searchParams.get("state");
  const error = searchParams.get("error");

  if (error) {
    log.warn("integration", `Google OAuth denied: ${error}`);
    return NextResponse.redirect(`${appUrl}/integrations?error=denied`);
  }

  if (!code || !stateParam) {
    return NextResponse.redirect(`${appUrl}/integrations?error=missing_code`);
  }

  let brandId: string;
  let service: string;
  try {
    const state = JSON.parse(Buffer.from(stateParam, "base64url").toString());
    brandId = state.brandId;
    service = state.service;
  } catch {
    return NextResponse.redirect(`${appUrl}/integrations?error=invalid_state`);
  }

  if (!brandId || !service) {
    return NextResponse.redirect(`${appUrl}/integrations?error=invalid_state`);
  }

  // Brand-existence guard. Without this, the upsert below blows up
  // with a Prisma foreign-key constraint error when brandId is stale
  // (brand deleted mid-flow, state generated against a different
  // environment, etc.) — and the old catch handler URL-encoded the
  // raw SQL error into the redirect, which users saw as a giant
  // mangled query string. Fail-fast with a clean code instead, and
  // avoid burning the OAuth authorization code on a write we can't
  // complete.
  const brand = await prisma.brand.findUnique({
    where: { id: brandId },
    select: { id: true, name: true, domain: true },
  }).catch(() => null);

  if (!brand) {
    log.warn("integration", `OAuth callback: brand ${brandId} not found — state may be stale`, {
      metadata: { brandId, service },
    });
    return NextResponse.redirect(`${appUrl}/integrations?error=brand_not_found`);
  }

  const redirectUri = `${appUrl}/auth/google/callback`;

  try {
    log.info("integration", `OAuth token exchange starting`, {
      metadata: { service, brandId, redirectUri, hasClientId: !!GOOGLE_CLIENT_ID, hasClientSecret: !!GOOGLE_CLIENT_SECRET },
    });

    const tokenRes = await fetch("https://oauth2.googleapis.com/token", {
      method: "POST",
      headers: { "Content-Type": "application/x-www-form-urlencoded" },
      body: new URLSearchParams({
        code,
        client_id: GOOGLE_CLIENT_ID,
        client_secret: GOOGLE_CLIENT_SECRET,
        redirect_uri: redirectUri,
        grant_type: "authorization_code",
      }),
    });

    const tokenData = await tokenRes.json();

    if (!tokenRes.ok || !tokenData.access_token) {
      log.error("integration", `Token exchange failed: ${JSON.stringify(tokenData)}`);
      // Short opaque code — the raw Google error goes to logs, not the URL.
      return NextResponse.redirect(`${appUrl}/integrations?error=token_failed`);
    }

    await prisma.brandIntegration.upsert({
      where: { brandId_integrationId: { brandId, integrationId: service } },
      create: {
        brandId,
        integrationId: service,
        connected: true,
        status: "connected",
        lastSynced: new Date(),
      },
      update: {
        connected: true,
        status: "connected",
        lastSynced: new Date(),
        syncError: null,
        syncRetryCount: 0,
        // Reset the token-refresh failure counter so a fresh grant starts
        // with a clean slate. Without this, a prior invalid_grant run-up
        // to the 3-strike threshold persists across reconnects.
        refreshFailCount: 0,
      },
    });

    const expiresAt = tokenData.expires_in
      ? new Date(Date.now() + tokenData.expires_in * 1000).toISOString()
      : undefined;

    await saveCredentials(brandId, service, {
      accessToken: tokenData.access_token,
      refreshToken: tokenData.refresh_token ?? undefined,
      expiresAt,
      tokenType: tokenData.token_type,
      scope: tokenData.scope,
    });

    log.info("integration", `Google OAuth completed for ${service}`, {
      metadata: { brandId, service, hasRefreshToken: !!tokenData.refresh_token },
    });

    // ── Auto-match property ─────────────────────────────────────────
    // Disconnect wiped siteUrl / propertyId; a reconnect otherwise
    // lands in a "No site URL configured" state until the user
    // re-runs the picker. Try to pair a single GSC site / GA4
    // property with the brand's domain; on a unique match, save
    // it to credentials so the integration is immediately usable.
    // Ambiguous matches fall through to the existing picker.
    let autoMatched = false;
    try {
      if (brand.domain) {
        if (service === "gsc") {
          const siteUrl = await findMatchingGscSite(tokenData.access_token, brand.domain);
          if (siteUrl) {
            const existing = await getCredentials(brandId, service);
            await saveCredentials(brandId, service, {
              ...(existing ?? {}),
              accessToken: tokenData.access_token,
              refreshToken: tokenData.refresh_token ?? existing?.refreshToken ?? undefined,
              expiresAt,
              siteUrl,
              accountId: siteUrl,
              propertyId: siteUrl,
              propertyName: siteUrl,
            });
            autoMatched = true;
            log.info("integration", `GSC auto-matched site: ${siteUrl}`, { metadata: { brandId, siteUrl } });
          }
        } else if (service === "ga4") {
          const match = await findMatchingGa4Property(tokenData.access_token, brand.domain, brand.name);
          if (match) {
            const existing = await getCredentials(brandId, service);
            await saveCredentials(brandId, service, {
              ...(existing ?? {}),
              accessToken: tokenData.access_token,
              refreshToken: tokenData.refresh_token ?? existing?.refreshToken ?? undefined,
              expiresAt,
              propertyId: match.propertyId,
              propertyName: match.propertyName,
            });
            autoMatched = true;
            log.info("integration", `GA4 auto-matched property: ${match.propertyName} (${match.propertyId})`, {
              metadata: { brandId, propertyId: match.propertyId },
            });
          }
        }
      }
    } catch (matchErr) {
      // Auto-match is best-effort — a failure here should never
      // block the successful OAuth. The property picker handles it.
      log.warn("integration", `Auto-match failed for ${service}: ${matchErr instanceof Error ? matchErr.message : "unknown"}`, {
        metadata: { brandId, service },
      });
    }

    // Backfill sync only when we have a property to query. Without
    // siteUrl / propertyId the sync fails fast and flips the
    // integration to "error" before the user picks a property —
    // confusing right after a successful connect.
    if (autoMatched && (service === "gsc" || service === "ga4")) {
      syncIntegration(brandId, service).catch((err) => {
        log.warn("integration", `Post-OAuth backfill sync failed for ${service}: ${err instanceof Error ? err.message : "unknown"}`, {
          metadata: { brandId, service },
        });
      });
    }

    const qs = autoMatched ? `connected=${service}&autoMatched=1` : `connected=${service}`;
    return NextResponse.redirect(`${appUrl}/integrations?${qs}`);

  } catch (err) {
    // Full stack goes to logs; the URL gets a short opaque code so
    // Prisma / Google error strings never reach the browser.
    const msg = err instanceof Error ? err.message : "Unknown error";
    log.error("integration", `OAuth callback error for ${service}: ${msg}`);
    console.error("[oauth:callback] caught error", err);
    return NextResponse.redirect(`${appUrl}/integrations?error=callback_failed`);
  }
}
