import { ApolloClient, ApolloLink, InMemoryCache, split } from "@apollo/client";
import { setContext } from "@apollo/client/link/context";
import { HttpLink } from "@apollo/client/link/http";
import { RetryLink } from "@apollo/client/link/retry";
import { GraphQLWsLink } from "@apollo/client/link/subscriptions";
import { getMainDefinition } from "@apollo/client/utilities";

import apolloLogger from "apollo-link-logger";
import { createClient } from "graphql-ws";
import { map } from "rxjs/operators";

import { auth } from "../firebase/firebase.utils";
import errorLink from "../graphql/apollo-error-handling";

/**
 * HTTP transport
 */
const httpLink = new HttpLink({
  uri: import.meta.env.VITE_APP_GRAPHQL_ENDPOINT
});

/**
 * WS transport (graphql-ws)
 * - Auth header is established at connect-time via connectionParams.
 * - Reconnect behavior is handled here (NOT via RetryLink).
 */
const wsClient = createClient({
  url: import.meta.env.VITE_APP_GRAPHQL_ENDPOINT_WS,
  lazy: true,
  lazyCloseTimeout: 5000,

  retryAttempts: Infinity,
  shouldRetry: (eventOrError) => {
    // Don't retry a clean close (1000); retry everything else
    const code = eventOrError?.code;
    if (typeof code === "number") return code !== 1000;
    return true;
  },

  connectionParams: async () => {
    const token = await auth.currentUser?.getIdToken();
    return token
      ? {
          headers: {
            authorization: `Bearer ${token}`
          }
        }
      : {};
  }
});

const wsLink = new GraphQLWsLink(wsClient);

/**
 * DEV-only: attach a caller stack to every operation so errors can point back
 * to the initiating module/component.
 */
const callerStackLink = import.meta.env.DEV
  ? new ApolloLink((operation, forward) => {
      const stack = new Error().stack;

      operation.setContext((prev) => ({
        ...prev,
        callerStack: stack
      }));

      return forward(operation);
    })
  : ApolloLink.empty();

/**
 * Round-trip timing (HTTP only)
 * NOTE: Subscriptions are long-lived streams; timing/retry should not wrap them.
 */
const roundTripLink = new ApolloLink((operation, forward) => {
  operation.setContext({ start: Date.now() });

  const obs = forward?.(operation);
  if (!obs) return null;

  return obs.pipe(
    map((result) => {
      // const start = operation.getContext().start;
      // const timeMs = Date.now() - start;
      // TrackExecutionTime(operation.operationName, timeMs);
      return result;
    })
  );
});

/**
 * Auth for HTTP requests (per-request token)
 */
const authLink = setContext(async (_, { headers }) => {
  try {
    const token = await auth.currentUser?.getIdToken();
    if (!token) return { headers };

    return {
      headers: {
        ...headers,
        authorization: `Bearer ${token}`
      }
    };
  } catch (error) {
    console.error("Authentication error. Unable to add authorization token.", error?.message || error);
    return { headers };
  }
});

/**
 * Retry (HTTP only) — transient/network failures only.
 * Avoid retrying deterministic GraphQL errors (validation, missing variables, etc).
 */
const retryLink = new RetryLink({
  delay: {
    initial: 500,
    max: 5000,
    jitter: true
  },
  attempts: {
    max: 5,
    retryIf: (error) => {
      // Apollo's network errors often expose statusCode/response.status
      const status = error?.statusCode ?? error?.response?.status;
      if (typeof status === "number") {
        // Common transient HTTP statuses
        return [408, 429, 500, 502, 503, 504].includes(status);
      }

      // Fallback heuristic for fetch/socket transient failures
      const msg = `${error?.message || ""}`;
      return /network|fetch|timeout|ECONNRESET|ENOTFOUND|EAI_AGAIN/i.test(msg);
    }
  }
});

/**
 * Branch chains
 * - HTTP branch includes timing + retry + auth.
 * - WS branch is intentionally lean; graphql-ws handles reconnect.
 *
 * NOTE:
 * errorLink is applied OUTSIDE the split so it sees both HTTP + WS operations.
 * Placing errorLink upstream of retryLink means it generally logs only after retries are exhausted.
 */
const httpChain = ApolloLink.from([roundTripLink, retryLink, authLink, httpLink]);
const wsChain = ApolloLink.from([wsLink]);

const isSubscriptionOperation = ({ query }) => {
  const definition = getMainDefinition(query);
  return definition.kind === "OperationDefinition" && definition.operation === "subscription";
};

const terminatingLink = split(isSubscriptionOperation, wsChain, httpChain);

/**
 * Final link chain
 * - errorLink wraps both branches (HTTP + WS)
 * - callerStackLink adds call-site stack (DEV only)
 * - apollo-link-logger only in DEV
 */
const link = ApolloLink.from(
  import.meta.env.DEV ? [apolloLogger, callerStackLink, errorLink, terminatingLink] : [errorLink, terminatingLink]
);

/**
 * Cache policies
 */
const cache = new InMemoryCache({
  typePolicies: {
    users: {
      keyFields: ["email"]
    },
    masterdata: {
      keyFields: ["key"]
    },
    Query: {
      fields: {
        job_watchers: {
          merge(_existing, incoming) {
            return incoming;
          }
        },
        conversations: {
          keyArgs: ["where", "order_by"],
          merge(existing = [], incoming = [], { args, readField }) {
            const offset = args?.offset ?? 0;
            const merged = existing ? existing.slice(0) : [];

            for (let i = 0; i < incoming.length; i++) {
              merged[offset + i] = incoming[i];
            }

            // Deduplicate by id (important when you also upsert via sockets)
            const seen = new Set();
            return merged.filter((ref) => {
              const id = readField("id", ref);
              if (!id || seen.has(id)) return false;
              seen.add(id);
              return true;
            });
          }
        },
        notifications: {
          // Keep "respect the current query filter" behavior (i.e., don't union with existing),
          // but dedupe within the incoming page/list so UI doesn't flicker with duplicates.
          // eslint-disable-next-line no-unused-vars
          merge(_existing = [], incoming = [], { readField }) {
            const seen = new Set();
            const out = [];

            for (const item of incoming) {
              const typename = readField("__typename", item);
              const id = readField("id", item);

              const key = typename && id ? `${typename}:${id}` : null;

              if (!key) {
                out.push(item);
                continue;
              }

              if (seen.has(key)) continue;
              seen.add(key);
              out.push(item);
            }

            return out;
          }
        }
      }
    }
  }
});

/**
 * Client
 */
const client = new ApolloClient({
  link,
  cache,
  devtools: {
    name: "Imex Client",
    enabled: import.meta.env.DEV
  },
  defaultOptions: {
    watchQuery: {
      fetchPolicy: "network-only",
      nextFetchPolicy: "network-only",
      errorPolicy: "ignore",
      notifyOnNetworkStatusChange: false
    },
    query: {
      fetchPolicy: "network-only",
      errorPolicy: "all"
    },
    mutate: {
      errorPolicy: "all"
    }
  }
});

export default client;
