/**
 * GET /api/auth/google/callback
 * --------------------------------
 * Google redirects here after the user approves access.
 * We exchange the code for tokens, store them encrypted,
 * auto-match a GSC site / GA4 property against the brand's domain
 * (so a reconnect doesn't leave the integration stuck on "No site
 * URL configured"), and redirect back to the integrations page.
 */

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";

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();
}

/** Fetch GSC verified sites and return the one whose siteUrl matches
 *  the brand's domain, if exactly one match exists. Returns null on
 *  zero or multiple matches so the property picker can take over. */
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)
      // Prefer properties the user can actually query (siteOwner / siteFullUser)
      .sort((a, b) => {
        const score = (p?: string) => (p === "siteOwner" ? 2 : p === "siteFullUser" ? 1 : 0);
        return score(b.permissionLevel) - score(a.permissionLevel);
      });
    if (matches.length === 0) return null;
    // Return the best-ranked match. A domain property and a URL-prefix
    // property can both exist — picking one is better than no auto-match.
    return matches[0].siteUrl;
  } catch {
    return null;
  }
}

/** Fetch GA4 account summaries and return the propertyId + displayName
 *  whose property summary name contains the brand's domain or name.
 *  Returns null on zero or multiple matches. */
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,
        accountName: acc.displayName ?? "",
      })),
    );

    // Match priority: exact domain > brand name substring > account name
    // substring. GA4 property display names are free-form text — users
    // commonly name them after the site domain, sometimes after the
    // brand. We only auto-select when exactly one property matches at
    // the highest priority level we found a hit on; otherwise the
    // picker runs so the user can choose.
    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) {
  // Note: we don't require auth here because Google's redirect loses the
  // session. The brandId comes from the state parameter which we set.
  const appUrl = process.env.NEXT_PUBLIC_APP_URL || new URL(req.url).origin;

  const { searchParams } = new URL(req.url);
  const code = searchParams.get("code");
  const stateParam = searchParams.get("state");
  const error = searchParams.get("error");

  // Handle user denial
  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`);
  }

  // Decode state to get brandId and service
  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`);
  }

  // Exchange authorization code for tokens
  const redirectUri = `${appUrl}/api/auth/google/callback`;

  try {
    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", `Google token exchange failed: ${JSON.stringify(tokenData)}`);
      return NextResponse.redirect(`${appUrl}/integrations?error=token_failed`);
    }

    // Ensure the integration row exists
    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,
      },
    });

    // Store tokens encrypted
    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 the previously-selected siteUrl / propertyId,
    // so a reconnect would otherwise land in a "No site URL
    // configured" broken state until the user re-ran the picker.
    // Try to auto-match a single GSC site or GA4 property against
    // the brand's domain. On a unique match, save it to credentials
    // so the integration is immediately usable. On zero or multiple
    // matches the frontend picker still handles selection.
    let autoMatched = false;
    try {
      const brand = await prisma.brand.findUnique({
        where: { id: brandId },
        select: { name: true, domain: true },
      });
      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,
              // GSC stores both for compatibility with downstream
              // readers — some look at siteUrl, others at accountId.
              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 (err) {
      log.warn("integration", `Auto-match failed for ${service}: ${err instanceof Error ? err.message : "unknown"}`, {
        metadata: { brandId, service },
      });
    }

    // Fire-and-forget backfill sync ONLY when we have a property to
    // query. Without siteUrl / propertyId the sync fails fast and
    // flips the integration into "error" status, which the UI then
    // renders as red — confusing when the user just connected
    // successfully and is about to pick a property. If auto-match
    // failed, the property picker + its own sync-on-select path
    // (see /api/integrations/properties POST) handles the first
    // sync after the user makes a choice.
    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 },
        });
      });
    }

    // Redirect back to integrations. The query param differs so the
    // frontend knows whether to open the property picker:
    //   ?connected=ga4           → picker opens (no auto-match)
    //   ?connected=ga4&autoMatched=1 → skip picker, show success
    const qs = autoMatched ? `connected=${service}&autoMatched=1` : `connected=${service}`;
    return NextResponse.redirect(`${appUrl}/integrations?${qs}`);

  } catch (err) {
    const msg = err instanceof Error ? err.message : "Unknown error";
    log.error("integration", `Google OAuth callback error: ${msg}`);
    return NextResponse.redirect(`${appUrl}/integrations?error=callback_failed`);
  }
}
