"use client";

import {
  createContext,
  useCallback,
  useContext,
  useEffect,
  useMemo,
  useRef,
  useState,
} from "react";

import { useQueryClient } from "@tanstack/react-query";

import { agentAuthApi } from "@/api/agentAuth.api";
import type { AgentLoginData } from "@/api/agentAuth.api";
import { agentProfileApi } from "@/api/agentProfile.api";
import type { AgentProfileData } from "@/api/agentProfile.api";
import { AGENT_HOME } from "@/constants/routes";
import { useAgentAppSettings } from "@/hooks/queries/useAgentSettings";
import { useCrossTabSession } from "@/hooks/useCrossTabSession";
import { onAgentUnauthorized } from "@/lib/agentAuthBridge";
import { agentTokenStore } from "@/lib/agentTokenStore";
import { isAgentSessionQuery } from "@/lib/queryKeys";
import type { AgentUser, KycStatus } from "@/types";

const AGENT_USER_KEY = "xremit_agent_user";

export interface AgentSignInInput {
  email: string;
  password: string;
}

/** Where to send the agent next, so the page can route to the right challenge screen. */
export interface AgentSignInResult {
  destination: string;
  /** status=0 (admin banned) — no session was created. */
  banned: boolean;
}

interface AgentAuthContextValue {
  user: AgentUser | null;
  hydrated: boolean;
  signIn: (input: AgentSignInInput) => Promise<AgentSignInResult>;
  /** Promotes the session after a login/register challenge clears (email code,
   *  2FA otp) — pulls the authoritative agent from the bearer-authenticated
   *  token issued at login/register, since signIn withholds the session while
   *  a challenge is pending. Returns the raw profile so the caller can decide
   *  the next hop (kyc vs dashboard). */
  completeVerification: (fallbackEmail?: string) => Promise<AgentProfileData>;
  /** Store a token issued outside signIn (registration). It ends any previous session on
   *  this browser; promote the new one with completeVerification once no challenge remains. */
  beginSession: (token: string) => void;
  signOut: () => void;
  updateUser: (patch: Partial<AgentUser>) => void;
  /** Re-pull the account flags (KYC / email / 2FA) from the server into the cached agent. */
  refreshAccountStatus: () => Promise<void>;
}

const AgentAuthContext = createContext<AgentAuthContextValue | undefined>(undefined);

// ─── Local persistence ────────────────────────────────────────────────────────

function readUser(): AgentUser | null {
  try {
    const raw = typeof window !== "undefined" ? localStorage.getItem(AGENT_USER_KEY) : null;
    return raw ? (JSON.parse(raw) as AgentUser) : null;
  } catch {
    return null;
  }
}

/** The session as storage has it. A cached agent counts only alongside a token — signIn
 *  stores the token before a pending 2FA/email challenge clears, and that's no session. */
function readSession(): AgentUser | null {
  return agentTokenStore.isAuthenticated() ? readUser() : null;
}

function persistUser(user: AgentUser | null): void {
  try {
    if (user) localStorage.setItem(AGENT_USER_KEY, JSON.stringify(user));
    else localStorage.removeItem(AGENT_USER_KEY);
  } catch { /* ignore */ }
}

// ─── Response adapter ────────────────────────────────────────────────────────

// Login response only carries a binary flag (0|1), but the KYC endpoint reports the
// full 0-3 range (unverified/verified/pending/rejected) — widen to cover both.
function kycFromFlag(flag: number): KycStatus {
  if (flag === 1) return "verified";
  if (flag === 2) return "pending";
  if (flag === 3) return "rejected";
  return "unverified";
}

function mapAgentUser(agent: AgentLoginData["agent"], fallbackEmail = ""): AgentUser {
  return {
    id: String(agent.id) || fallbackEmail,
    email: agent.email || fallbackEmail,
    phone: agent.full_mobile || agent.mobile || "",
    firstName: agent.firstname,
    lastName: agent.lastname,
    storeName: agent.store_name,
    avatar: agent.agentImage || undefined,
    kycStatus: kycFromFlag(agent.kyc_verified),
    emailVerified: agent.email_verified === 1,
    twoFactorEnabled: agent.two_factor_status === 1,
    createdAt: agent.created_at,
  };
}

function mapAgentProfile(info: AgentProfileData, fallbackEmail = ""): AgentUser {
  return {
    id: String(info.id) || fallbackEmail,
    email: info.email || fallbackEmail,
    phone: info.full_mobile || info.mobile || "",
    firstName: info.firstname,
    lastName: info.lastname,
    storeName: info.store_name,
    avatar: info.agentImage || undefined,
    kycStatus: kycFromFlag(info.kyc_verified ?? 0),
    emailVerified: info.email_verified === 1,
    twoFactorEnabled: info.two_factor_status === 1,
    createdAt: info.lastLogin ?? "",
  };
}

// ─── Provider ────────────────────────────────────────────────────────────────

export function AgentAuthProvider({ children }: { children: React.ReactNode }) {
  const [user, setUser] = useState<AgentUser | null>(null);
  const [hydrated, setHydrated] = useState(false);
  const { emailVerificationRequired, kycVerificationRequired } = useAgentAppSettings();
  const queryClient = useQueryClient();

  // Cached data that belonged to the previous session. Public queries survive.
  const dropSessionCache = useCallback(
    () => queryClient.removeQueries({ predicate: (query) => isAgentSessionQuery(query.queryKey) }),
    [queryClient],
  );

  // The account-status check still on the wire, if any. Logout revokes the token
  // server-side, so a check that outlives the session can only come back as a stray
  // "unauthorized" error — it's cancelled when the session ends.
  const statusCheckRef = useRef<AbortController | null>(null);

  // Tear down this tab's in-memory session. Doesn't touch storage, so another tab's
  // sign-out (or its pending 2FA/email challenge token) is mirrored, never overwritten.
  const resetLocalSession = useCallback(() => {
    statusCheckRef.current?.abort();
    setUser(null);
    dropSessionCache();
  }, [dropSessionCache]);

  const clearSession = useCallback(() => {
    agentTokenStore.clear();
    persistUser(null);
    resetLocalSession();
  }, [resetLocalSession]);

  // Cached agent (localStorage) only reflects state as of last login — an admin-side
  // change (KYC decision, a ban) made afterward never reaches it otherwise. Re-pull
  // the authoritative status on (re)hydrate and on window focus.
  const refreshAccountStatus = useCallback(async () => {
    // The backend rejects profile reads (400) while a 2FA/email challenge is pending.
    if (!readSession()) return;
    // Only the latest check matters (e.g. rapid focus events) — drop a superseded one.
    statusCheckRef.current?.abort();
    const controller = new AbortController();
    statusCheckRef.current = controller;
    try {
      const info = await agentProfileApi.info(controller.signal);
      // Admin banned the agent mid-session — tear it down immediately, same as a
      // hard 401 from the interceptor.
      if (info.status === 0) {
        clearSession();
        return;
      }
      // These 3 flags are admin-togglable server-side (KYC / email / 2FA verification
      // panels) — resync all of them, not just KYC, so an out-of-band admin change
      // shows up without forcing a logout. Fields the profile endpoint omits keep
      // the previously-known value instead of being reset to a fabricated default.
      const nextKycStatus = kycFromFlag(info.kyc_verified ?? 0);
      const rawEmailVerified = info.email_verified;
      const rawTwoFactorStatus = info.two_factor_status;
      setUser((prev) => {
        if (!prev) return prev;
        const nextEmailVerified = rawEmailVerified === undefined ? prev.emailVerified : rawEmailVerified === 1;
        const nextTwoFactorEnabled = rawTwoFactorStatus === undefined ? prev.twoFactorEnabled : rawTwoFactorStatus === 1;
        if (
          prev.kycStatus === nextKycStatus &&
          prev.emailVerified === nextEmailVerified &&
          prev.twoFactorEnabled === nextTwoFactorEnabled
        ) return prev;
        const next = {
          ...prev,
          kycStatus: nextKycStatus,
          emailVerified: nextEmailVerified,
          twoFactorEnabled: nextTwoFactorEnabled,
        };
        persistUser(next);
        return next;
      });
    } catch {
      /* Network/auth failure or cancelled — keep last-known cached status. */
    } finally {
      if (statusCheckRef.current === controller) statusCheckRef.current = null;
    }
  }, [clearSession]);

  useEffect(() => {
    setUser(readSession());
    setHydrated(true);
    refreshAccountStatus();
    const unsubscribe = onAgentUnauthorized(clearSession);
    window.addEventListener("focus", refreshAccountStatus);
    return () => {
      unsubscribe();
      window.removeEventListener("focus", refreshAccountStatus);
    };
  }, [clearSession, refreshAccountStatus]);

  useCrossTabSession({
    userKey: AGENT_USER_KEY,
    readSession,
    onSignedOut: resetLocalSession,
    onUpdated: setUser,
  });

  const signIn = useCallback(async ({ email, password }: AgentSignInInput): Promise<AgentSignInResult> => {
    const res = await agentAuthApi.login(email, password);
    const { token, agent } = res;

    // status=0 → account banned by admin. No token, no session — the credentials
    // were valid but the account itself is blocked.
    if (agent.status === 0) {
      return { destination: "/agent/login", banned: true };
    }

    if (token) {
      agentTokenStore.setTokens({ accessToken: token });
      // A new token ends any previous session on this browser — never pair it with the
      // previous agent's cached data.
      dropSessionCache();
    }

    const requiresTwoFactor = !!agent.two_factor_status && !agent.two_factor_verified;
    const requiresEmailVerification = emailVerificationRequired && !agent.email_verified;

    // Only promote to an active session once no further challenge is pending. Until then
    // no agent is signed in — a previous one must not stay paired with the new token.
    const mapped = requiresTwoFactor || requiresEmailVerification ? null : mapAgentUser(agent, email);
    persistUser(mapped);
    setUser(mapped);

    const emailParam = `email=${encodeURIComponent(email)}`;
    if (requiresTwoFactor) return { destination: `/agent/verify-2fa?${emailParam}`, banned: false };
    if (requiresEmailVerification) return { destination: `/agent/verify-email?${emailParam}`, banned: false };
    // sms_verified: no API endpoint exists — skip gate
    if (kycVerificationRequired && !agent.kyc_verified) return { destination: "/agent/kyc", banned: false };
    return { destination: AGENT_HOME, banned: false };
  }, [emailVerificationRequired, kycVerificationRequired, dropSessionCache]);

  const completeVerification = useCallback(async (fallbackEmail = ""): Promise<AgentProfileData> => {
    const info = await agentProfileApi.info();
    const mapped = mapAgentProfile(info, fallbackEmail);
    persistUser(mapped);
    setUser(mapped);
    return info;
  }, []);

  const beginSession = useCallback((token: string) => {
    agentTokenStore.setTokens({ accessToken: token });
    persistUser(null);
    resetLocalSession();
  }, [resetLocalSession]);

  const signOut = useCallback(() => {
    const token = agentTokenStore.getAccessToken() ?? undefined;
    clearSession();
    agentAuthApi.logout(token).catch(() => undefined);
  }, [clearSession]);

  const updateUser = useCallback((patch: Partial<AgentUser>) => {
    setUser((prev) => {
      if (!prev) return prev;
      const next = { ...prev, ...patch };
      persistUser(next);
      return next;
    });
  }, []);

  const value = useMemo<AgentAuthContextValue>(
    () => ({ user, hydrated, signIn, completeVerification, beginSession, signOut, updateUser, refreshAccountStatus }),
    [user, hydrated, signIn, completeVerification, beginSession, signOut, updateUser, refreshAccountStatus],
  );

  return <AgentAuthContext.Provider value={value}>{children}</AgentAuthContext.Provider>;
}

export function useAgentAuth() {
  const ctx = useContext(AgentAuthContext);
  if (!ctx) throw new Error("useAgentAuth must be used within AgentAuthProvider");
  return ctx;
}
