import type { NextAuthOptions } from "next-auth";
import type { JWT } from "next-auth/jwt";
import AuthentikProvider from "next-auth/providers/authentik";
import { useGisAcFlag } from "@/core/feature-flags/use-gis-ac";

/**
 * Refresh the Authentik access_token using the stored refresh_token.
 * Authentik provider pk=6 issues access tokens valid for 5 minutes; the
 * refresh token is good for 30 days. NextAuth's JWT cookie lasts longer
 * (30 days default), so without refresh the access_token in session goes
 * stale after 5 min → api.gis.ac responds invalid_token.
 *
 * Marks the JWT with token.error="RefreshAccessTokenError" on failure;
 * the client can react by forcing a re-login.
 *
 * **Concurrency:** Authentik rotates refresh_tokens — a refresh_token can
 * only be exchanged once. With parallel requests (typical for a map app —
 * map tiles + parcela.get + search all firing at once), each request
 * triggers its own jwt callback, each calls refresh, only the first wins
 * and the rest get `invalid_grant` → user is logged out for no good
 * reason. We dedupe in-memory by refresh_token key so concurrent callers
 * share a single Authentik request.
 */
const inflightRefreshes = new Map<string, Promise<JWT>>();

async function refreshAuthentikToken(token: JWT): Promise<JWT> {
  const refreshToken = token.refreshToken ? String(token.refreshToken) : "";
  if (!refreshToken) {
    return { ...token, error: "RefreshAccessTokenError" };
  }

  // Dedupe concurrent refreshes for the same refresh_token.
  const existing = inflightRefreshes.get(refreshToken);
  if (existing) {
    return existing.then((fresh) => ({ ...token, ...fresh }));
  }

  const promise = (async (): Promise<JWT> => {
    try {
      const issuer = process.env.AUTHENTIK_ISSUER;
      const clientId = process.env.AUTHENTIK_CLIENT_ID;
      const clientSecret = process.env.AUTHENTIK_CLIENT_SECRET;
      if (!issuer || !clientId || !clientSecret) {
        throw new Error("refresh_prerequisites_missing");
      }
      const url = `${issuer.replace(/\/$/, "")}/token/`;
      const res = await fetch(url, {
        method: "POST",
        headers: { "Content-Type": "application/x-www-form-urlencoded" },
        body: new URLSearchParams({
          grant_type: "refresh_token",
          refresh_token: refreshToken,
          client_id: clientId,
          client_secret: clientSecret,
        }),
        cache: "no-store",
        signal: AbortSignal.timeout(8_000),
      });
      const body = (await res.json()) as {
        access_token?: string;
        refresh_token?: string;
        expires_in?: number;
        error?: string;
      };
      if (!res.ok || !body.access_token) {
        console.warn("[auth] refresh failed:", res.status, body.error);
        return { ...token, error: "RefreshAccessTokenError" };
      }
      console.log("[auth] refresh OK expires_in=%d", body.expires_in ?? 300);
      return {
        ...token,
        accessToken: body.access_token,
        accessTokenExpires: Date.now() + (body.expires_in ?? 300) * 1000,
        refreshToken: body.refresh_token ?? refreshToken,
        error: undefined,
      };
    } catch (err) {
      const msg = err instanceof Error ? err.message : String(err);
      console.warn("[auth] refresh error:", msg);
      return { ...token, error: "RefreshAccessTokenError" };
    } finally {
      inflightRefreshes.delete(refreshToken);
    }
  })();

  inflightRefreshes.set(refreshToken, promise);
  return promise;
}

export const authOptions: NextAuthOptions = {
  providers: [
    AuthentikProvider({
      clientId: process.env.AUTHENTIK_CLIENT_ID || "",
      clientSecret: process.env.AUTHENTIK_CLIENT_SECRET || "",
      issuer: process.env.AUTHENTIK_ISSUER || "",
      authorization: {
        params: {
          scope:
            process.env.AUTHENTIK_SCOPES ||
            "openid email profile enrichment",
        },
      },
    }),
  ],
  callbacks: {
    async jwt({ token, user, profile, account }) {
      // First sign-in: capture Authentik OIDC access_token + refresh_token
      // + expiry so we can forward to api.gis.ac and refresh before stale.
      if (account?.access_token) {
        token.accessToken = account.access_token;
        token.refreshToken = account.refresh_token;
        token.accessTokenExpires = account.expires_at
          ? account.expires_at * 1000
          : Date.now() + 5 * 60 * 1000;
      }
      if (user) {
        token.id = user.id;
      }
      if (profile) {
        // Map Authentik groups/roles to our internal roles
        const groups = (profile as any).groups || [];
        let role = "user";
        if (groups.includes("architools-admin")) role = "admin";
        else if (groups.includes("architools-manager")) role = "manager";

        token.role = role;

        // Map company based on groups
        let company = "group";
        if (groups.includes("company-beletage")) company = "beletage";
        else if (groups.includes("company-urban-switch"))
          company = "urban-switch";
        else if (groups.includes("company-studii-de-teren"))
          company = "studii-de-teren";

        token.company = company;
      }

      // Refresh the access_token when within 30s of expiry. Skip if we
      // never captured one (no refresh_token to use) or refresh recently
      // failed (avoid hot-loop — user must re-login).
      const exp = typeof token.accessTokenExpires === "number"
        ? token.accessTokenExpires
        : 0;
      if (
        token.accessToken &&
        token.refreshToken &&
        token.error !== "RefreshAccessTokenError" &&
        Date.now() > exp - 30_000
      ) {
        return refreshAuthentikToken(token);
      }
      return token;
    },
    async session({ session, token }) {
      if (session.user) {
        (session.user as any).id = token.id;
        (session.user as any).role = token.role || "user";
        (session.user as any).company = token.company || "group";
      }
      (session as any).accessToken = token.accessToken;
      // Surface refresh failure so the client can force a re-login UX.
      if (token.error) (session as any).error = token.error;
      // Faza C cutover flag — exposed on session so client components can
      // branch the same way server routes do (env-driven, evaluated per
      // request so flag flip + container restart picks up without rebuild).
      (session as any).useGisAc = useGisAcFlag(session.user?.email);
      return session;
    },
  },
};
