import * as Sentry from "@sentry/react";
import { ErrorLink } from "@apollo/client/link/error";
import { CombinedGraphQLErrors, CombinedProtocolErrors } from "@apollo/client/errors";
import { print } from "graphql";
import { getMainDefinition } from "@apollo/client/utilities";
import { auth } from "../firebase/firebase.utils";
import { from, throwError } from "rxjs";
import { catchError, mergeMap } from "rxjs/operators";

const SENSITIVE_KEYS = new Set([
  "password",
  "token",
  "authorization",
  "accessToken",
  "refreshToken",
  "idToken",
  "session"
]);

let refreshInFlight = null;

const refreshTokenOnce = () => {
  if (!refreshInFlight) {
    refreshInFlight = auth.currentUser ? auth.currentUser.getIdToken(true) : Promise.resolve(null);
    refreshInFlight.finally(() => {
      refreshInFlight = null;
    });
  }
  return refreshInFlight;
};

const redactVariables = (variables) => {
  if (!variables || typeof variables !== "object") return variables;
  const out = { ...variables };
  for (const key of Object.keys(out)) {
    if (SENSITIVE_KEYS.has(key)) out[key] = "[REDACTED]";
  }
  return out;
};

const safePrintQuery = (query) => {
  try {
    return query ? print(query) : undefined;
  } catch {
    return undefined;
  }
};

const isSubscriptionOperation = (operation) => {
  try {
    const def = getMainDefinition(operation.query);
    return def?.kind === "OperationDefinition" && def?.operation === "subscription";
  } catch {
    return false;
  }
};

const extractGraphQLErrors = ({ error, result }) => {
  // Primary: Apollo Client 4+ GraphQL errors
  if (CombinedGraphQLErrors.is(error)) return error.errors;

  // Fallback: raw GraphQL response errors (what apollo-link-logger shows)
  if (Array.isArray(result?.errors) && result.errors.length) return result.errors;

  // Fallback: some “network-ish” errors still carry result.errors
  if (Array.isArray(error?.result?.errors) && error.result.errors.length) return error.result.errors;

  return null;
};

const hasUnauthCode = (errs) => Array.isArray(errs) && errs.some((e) => e?.extensions?.code === "UNAUTHENTICATED");

const errorLink = new ErrorLink(({ error, result, operation, forward }) => {
  const operationName = operation?.operationName || "anonymous";
  const ctx = operation?.getContext?.() || {};
  const callerStack = ctx?.callerStack;

  const variables = redactVariables(operation?.variables);
  const queryText = safePrintQuery(operation?.query);

  const gqlErrors = extractGraphQLErrors({ error, result });
  const protocolErrors = CombinedProtocolErrors.is(error) ? error.errors : null;

  // ---- Single auth-refresh retry (HTTP only, once) ----
  // Only retry if:
  // - we have GraphQL errors with UNAUTHENTICATED
  // - not a subscription
  // - not already retried
  // - forward exists (so we can re-run)
  if (forward && !isSubscriptionOperation(operation) && !ctx.__didAuthRetry && hasUnauthCode(gqlErrors)) {
    operation.setContext({ ...ctx, __didAuthRetry: true });

    return from(refreshTokenOnce()).pipe(
      mergeMap(() => forward(operation)),
      catchError(() => throwError(() => error))
    );
  }

  // ---- GraphQL errors ----
  if (gqlErrors?.length) {
    // Make it show up in the “Errors” filter (red)
    console.error(`[GraphQL error] ${operationName}`, {
      message: gqlErrors[0]?.message
    });

    if (import.meta.env.DEV) {
      console.groupCollapsed?.(`[GraphQL error details] ${operationName}`);
      console.log({
        operationName,
        variables,
        query: queryText,
        callerStack
      });
      gqlErrors.forEach((e, i) => {
        console.log(`Error #${i + 1}:`, {
          message: e.message,
          path: e.path,
          locations: e.locations,
          extensions: e.extensions
        });
      });
      console.groupEnd?.();
    }

    Sentry.withScope((scope) => {
      scope.setLevel("error");
      scope.setTag("graphql.operationName", operationName);

      scope.setContext("graphql.request", {
        operationName,
        variables,
        query: import.meta.env.DEV ? queryText : undefined
      });

      if (callerStack) scope.setContext("graphql.caller", { stack: callerStack });

      scope.setContext(
        "graphql.errors",
        gqlErrors.map((e) => ({
          message: e.message,
          path: e.path,
          locations: e.locations,
          extensions: e.extensions
        }))
      );

      Sentry.captureMessage(`[GraphQL] ${operationName}`);
    });

    // IMPORTANT: do NOT return forward(operation) unless intentionally retrying
    return;
  }

  // ---- Protocol errors (rare, but good to log) ----
  if (protocolErrors?.length) {
    console.error(`[Protocol error] ${operationName}`, {
      message: protocolErrors[0]?.message
    });

    if (import.meta.env.DEV) {
      console.groupCollapsed?.(`[Protocol error details] ${operationName}`);
      console.log({ operationName, variables, query: queryText, callerStack });
      protocolErrors.forEach((e, i) => console.log(`Protocol Error #${i + 1}:`, e));
      console.groupEnd?.();
    }

    Sentry.withScope((scope) => {
      scope.setLevel("error");
      scope.setTag("graphql.operationName", operationName);
      scope.setContext("graphql.request", {
        operationName,
        variables,
        query: import.meta.env.DEV ? queryText : undefined
      });
      if (callerStack) scope.setContext("graphql.caller", { stack: callerStack });
      scope.setContext("graphql.protocolErrors", protocolErrors);
      Sentry.captureMessage(`[GraphQL Protocol] ${operationName}`);
    });

    return;
  }

  // ---- Network / other errors ----
  if (error) {
    console.error(`[Network error] ${operationName}`, error);

    Sentry.withScope((scope) => {
      scope.setLevel("error");
      scope.setTag("graphql.operationName", operationName);
      scope.setContext("graphql.request", {
        operationName,
        variables,
        query: import.meta.env.DEV ? queryText : undefined
      });
      if (callerStack) scope.setContext("graphql.caller", { stack: callerStack });
      Sentry.captureException(error);
    });
  }
});

export default errorLink;
