"use client";

import {
  createContext,
  useCallback,
  useContext,
  useEffect,
  useMemo,
  useRef,
  useState,
} from "react";

import { useQueryClient } from "@tanstack/react-query";

import { authApi, type LoginResponse } from "@/api/auth.api";
import { profileApi, type ProfileInfoData } from "@/api/profile.api";
import { useAppSettings } from "@/hooks/queries/useSettings";
import { useCrossTabSession } from "@/hooks/useCrossTabSession";
import { onUnauthorized } from "@/lib/authBridge";
import { delay } from "@/lib/mockApi";
import { isUserSessionQuery } from "@/lib/queryKeys";
import { tokenStore } from "@/lib/tokenStore";
import type { KycProfile, KycStatus, User } from "@/types";

// Session user is cached here for hydration across reloads (it's profile data, not a
// secret — the bearer token lives in tokenStore). The Auth-folder routes are wired to
// the real API; profile/KYC methods below remain mock-backed (out of scope — Profile
// folder) until those folders are implemented.
const USER_KEY = "xremit_user";
const KYC_KEY = "xremit_kyc";
const KYC_VERIFY_DELAY_MS = 5000;

export interface SignInInput {
  email: string;
  password: string;
}

export interface SignUpInput {
  firstname: string;
  lastname: string;
  email: string;
  password: string;
  policy: boolean;
}

/** What the login step resolved to, so the page can route to the right next screen. */
export interface SignInResult {
  requiresTwoFactor: boolean;
  requiresEmailVerification: boolean;
  /** Account status=0 (admin banned) — no session was created. */
  banned: boolean;
}

interface AuthContextValue {
  user: User | null;
  hydrated: boolean;
  kycProfile: KycProfile | null;
  signIn: (input: SignInInput) => Promise<SignInResult>;
  signUp: (input: SignUpInput) => Promise<User>;
  verifyTwoFactor: (otp: string) => Promise<User>;
  completeEmailVerification: () => Promise<User>;
  signOut: () => void;
  updateProfile: (patch: Partial<User>) => Promise<User>;
  /** Re-pull the account flags (KYC / email / 2FA) from the server into the cached user. */
  refreshAccountStatus: () => Promise<void>;
  setKycStatus: (status: KycStatus) => void;
  saveKycProfile: (profile: KycProfile) => Promise<void>;
}

const AuthContext = createContext<AuthContextValue | undefined>(undefined);

// ─── Response adapters ─────────────────────────────────────────────────────────
// The backend user shape isn't pinned by the Postman collection, so map defensively
// across the common field-name variants. Missing strings become "" (Rule 18 — no
// fabricated values; let the UI show gaps), not invented data.

function asRecord(value: unknown): Record<string, unknown> {
  return value && typeof value === "object" ? (value as Record<string, unknown>) : {};
}

function pickString(source: Record<string, unknown>, ...keys: string[]): string {
  for (const key of keys) {
    const value = source[key];
    if (typeof value === "string" && value) return value;
    if (typeof value === "number") return String(value);
  }
  return "";
}

function mapKycStatus(value: unknown): KycStatus {
  // Numeric: 0=unverified, 1=approved, 2=pending, 3=rejected (profile API instruction)
  const n = Number(value);
  if (!Number.isNaN(n) && String(value).trim() !== "") {
    if (n === 1) return "verified";
    if (n === 2) return "pending";
    if (n === 3) return "rejected";
    return "unverified";
  }
  const s = String(value ?? "").toLowerCase();
  if (s === "verified" || s === "approved") return "verified";
  if (s === "pending") return "pending";
  if (s === "rejected" || s === "declined") return "rejected";
  return "unverified";
}

function mapUser(raw: unknown, fallbackEmail = ""): User {
  const source = asRecord(raw);
  const email = pickString(source, "email") || fallbackEmail;
  return {
    id: pickString(source, "id", "user_id", "uuid") || email,
    email,
    phone: pickString(source, "phone", "full_mobile", "mobile"),
    firstName: pickString(source, "firstname", "first_name", "firstName"),
    lastName: pickString(source, "lastname", "last_name", "lastName"),
    avatar: pickString(source, "image", "avatar", "photo") || undefined,
    kycStatus: mapKycStatus(source.kyc_verified ?? source.kyc_status ?? source.kyc ?? source.kycStatus),
    emailVerified: Boolean(source.email_verified ?? source.emailVerified ?? false),
    twoFactorEnabled: Boolean(source.two_factor_status ?? source.google2fa_status ?? false),
    createdAt: pickString(source, "created_at", "createdAt"),
    balance: Number(source.balance ?? 0) || 0,
  };
}

function extractToken(res: LoginResponse): string | undefined {
  const token = res.token;
  return typeof token === "string" ? token : undefined;
}

function extractUser(res: LoginResponse, fallbackEmail: string): User {
  // After unwrap: token + user are siblings at the top level of the response data.
  return mapUser(res.user ?? {}, fallbackEmail);
}

// ─── Local persistence ───────────────────────────────────────────────────────────

function readUser(): User | null {
  try {
    const raw = localStorage.getItem(USER_KEY);
    return raw ? (JSON.parse(raw) as User) : null;
  } catch {
    return null;
  }
}

/** The session as storage has it. A cached user counts only alongside a token — signIn
 *  stores the token before a pending 2FA/email challenge clears, and that's no session. */
function readSession(): User | null {
  return tokenStore.isAuthenticated() ? readUser() : null;
}

function readKyc(): KycProfile | null {
  try {
    const raw = localStorage.getItem(KYC_KEY);
    return raw ? (JSON.parse(raw) as KycProfile) : null;
  } catch {
    return null;
  }
}

function persistUser(user: User | null) {
  try {
    if (user) localStorage.setItem(USER_KEY, JSON.stringify(user));
    else localStorage.removeItem(USER_KEY);
  } catch {
    /* ignore */
  }
}

function persistKyc(profile: KycProfile | null) {
  try {
    if (profile) localStorage.setItem(KYC_KEY, JSON.stringify(profile));
    else localStorage.removeItem(KYC_KEY);
  } catch {
    /* ignore */
  }
}

export function AuthProvider({ children }: { children: React.ReactNode }) {
  const [user, setUser] = useState<User | null>(null);
  const [kycProfile, setKycProfile] = useState<KycProfile | null>(null);
  const [hydrated, setHydrated] = useState(false);
  const { emailVerificationRequired } = useAppSettings();
  const queryClient = useQueryClient();

  // In-flight KYC auto-verify timer (demo, mock — Profile folder). Lives on a ref so it
  // survives route changes and can be cleared on signOut / re-submission.
  const verifyTimerRef = useRef<number | null>(null);
  const clearVerifyTimer = useCallback(() => {
    if (verifyTimerRef.current !== null) {
      window.clearTimeout(verifyTimerRef.current);
      verifyTimerRef.current = null;
    }
  }, []);

  // Cached data that belonged to the previous session. Public queries survive.
  const dropSessionCache = useCallback(
    () => queryClient.removeQueries({ predicate: (query) => isUserSessionQuery(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
  // "not authorized" 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(() => {
    clearVerifyTimer();
    statusCheckRef.current?.abort();
    setUser(null);
    dropSessionCache();
  }, [clearVerifyTimer, dropSessionCache]);

  const clearSession = useCallback(() => {
    tokenStore.clear();
    persistUser(null);
    resetLocalSession();
  }, [resetLocalSession]);

  // The cached user (localStorage) only reflects state as of last sign-in/update — it
  // never picks up an admin-side change (e.g. KYC approval, or a ban) made out-of-band.
  // Re-pull the authoritative status from the server whenever the app (re)hydrates or
  // regains focus, instead of trusting the stale snapshot indefinitely.
  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 profileApi.info(controller.signal)) as ProfileInfoData;
      // Admin banned the account mid-session — tear it down immediately, same as a
      // hard 401 from the interceptor.
      if (info.user_info?.status === 0) {
        clearSession();
        return;
      }
      // Re-map identity + verification flags from this authoritative response — not
      // just kyc_verified — so a cached user ever poisoned with blank name/email/phone
      // (e.g. by a challenge endpoint that omitted them) self-heals here instead of
      // staying broken until a fresh login. `balance` and `createdAt` aren't part of
      // this endpoint's payload, so those two are carried over from the cached value.
      setUser((prev) => {
        if (!prev) return prev;
        const mapped = mapUser(info.user_info, prev.email);
        const next: User = { ...mapped, balance: prev.balance, createdAt: mapped.createdAt || prev.createdAt };
        const unchanged = (Object.keys(next) as (keyof User)[]).every((k) => next[k] === prev[k]);
        if (unchanged) return prev;
        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());
    setKycProfile(readKyc());
    setHydrated(true);
    refreshAccountStatus();

    // A forced logout from the axios interceptor (unrecoverable 401) tears down here;
    // the (app) layout guard then redirects to /login.
    const unsubscribe = onUnauthorized(clearSession);
    window.addEventListener("focus", refreshAccountStatus);
    return () => {
      unsubscribe();
      clearVerifyTimer();
      window.removeEventListener("focus", refreshAccountStatus);
    };
  }, [clearSession, clearVerifyTimer, refreshAccountStatus]);

  useCrossTabSession({
    userKey: USER_KEY,
    readSession,
    onSignedOut: resetLocalSession,
    onUpdated: setUser,
  });

  const signIn = useCallback(async ({ email, password }: SignInInput): Promise<SignInResult> => {
    const res = await authApi.login({ email, password });

    // status=0 → account banned by admin. No token, no session — the credentials
    // were valid but the account itself is blocked.
    if (res.user?.status === 0) {
      return { requiresTwoFactor: false, requiresEmailVerification: false, banned: true };
    }

    const token = extractToken(res);
    if (token) {
      tokenStore.setTokens({ accessToken: token });
      // A new token ends any previous session on this browser — never pair it with the
      // previous account's cached data.
      dropSessionCache();
    }

    // Flags live inside res.user (after unwrap strips the outer `data` envelope).
    // two_factor_status=1 → 2FA enabled; two_factor_verified=1 → already passed this
    // session's challenge (e.g. a trusted-device skip) — only prompt when neither.
    // email_verified=0 → email not confirmed, but only gate on it while the platform's
    // own email-verification toggle is on (admin can disable the requirement entirely).
    const requiresTwoFactor = res.user?.two_factor_status === 1 && res.user?.two_factor_verified !== 1;
    const requiresEmailVerification = emailVerificationRequired && res.user?.email_verified === 0;

    // Only promote to an active session once no further challenge is pending. Until then
    // no account is signed in — a previous one must not stay paired with the new token.
    const mapped = requiresTwoFactor || requiresEmailVerification ? null : extractUser(res, email);
    persistUser(mapped);
    setUser(mapped);
    return { requiresTwoFactor, requiresEmailVerification, banned: false };
  }, [emailVerificationRequired, dropSessionCache]);

  const signUp = useCallback(async (input: SignUpInput): Promise<User> => {
    const res = await authApi.register(input);
    const token = extractToken(res);
    if (token) {
      tokenStore.setTokens({ accessToken: token });
      dropSessionCache();
    }
    const mapped = extractUser(res, input.email);
    persistUser(mapped);
    setUser(mapped);
    return mapped;
  }, [dropSessionCache]);

  const verifyTwoFactor = useCallback(async (otp: string): Promise<User> => {
    const res = (await authApi.verifyTwoFactor({ otp })) as LoginResponse;
    const token = extractToken(res);
    if (token) tokenStore.setTokens({ accessToken: token });
    // The OTP-verify response only confirms the challenge — it doesn't reliably carry
    // kyc_verified / two_factor_status / email_verified. Pull the authoritative profile
    // (same pattern as completeEmailVerification) instead of defaulting those flags to
    // false/"unverified", which was flashing the KYC/2FA nudges until the next
    // refreshAccountStatus() silently corrected them.
    const info = (await profileApi.info()) as ProfileInfoData;
    const mapped = mapUser(info.user_info, user?.email ?? res.user?.email ?? readUser()?.email ?? "");
    persistUser(mapped);
    setUser(mapped);
    return mapped;
  }, [user]);

  // Promotes the session after the registration-code challenge (register?verify=1,
  // reached from login when email_verified=0). signIn withholds the session until
  // this challenge clears, so this is what actually creates it — pulls the
  // authoritative user from the bearer-authenticated token issued at login.
  const completeEmailVerification = useCallback(async (): Promise<User> => {
    const info = (await profileApi.info()) as ProfileInfoData;
    const mapped = mapUser(info.user_info, user?.email ?? readUser()?.email ?? "");
    persistUser(mapped);
    setUser(mapped);
    return mapped;
  }, [user]);

  const signOut = useCallback(() => {
    // Capture token before clearSession() wipes the store — axios interceptor runs as
    // a microtask (async), so it would read null if we cleared first.
    const token = tokenStore.getAccessToken() ?? undefined;
    clearSession();
    authApi.logout(token).catch(() => undefined);
  }, [clearSession]);

  // ─── Profile ──────────────────────────────────────────────────────────────────
  const updateProfile = useCallback(async (patch: Partial<User>): Promise<User> => {
    // Map app-layer User fields → API input. `avatar` is a URL string; image upload
    // requires a File and must go through the dedicated file-upload flow, not here.
    await profileApi.update({
      firstname: patch.firstName,
      lastname: patch.lastName,
      phone: patch.phone,
      country: (patch as Record<string, unknown>).country as string | undefined,
      city: (patch as Record<string, unknown>).city as string | undefined,
      state: (patch as Record<string, unknown>).state as string | undefined,
      address: (patch as Record<string, unknown>).address as string | undefined,
    });
    // Re-fetch authoritative profile state from server
    const info = (await profileApi.info()) as ProfileInfoData;
    const mapped = mapUser(info.user_info, user?.email ?? "");
    persistUser(mapped);
    setUser(mapped);
    return mapped;
  }, [user]);

  const setKycStatus = useCallback((status: KycStatus) => {
    const current = readUser();
    if (!current) return;
    const next = { ...current, kycStatus: status };
    persistUser(next);
    setUser(next);
  }, []);

  const saveKycProfile = useCallback(
    async (profile: KycProfile) => {
      await delay();
      persistKyc(profile);
      setKycProfile(profile);
      const current = readUser();
      if (!current) return;
      const next = { ...current, kycStatus: "pending" as KycStatus };
      persistUser(next);
      setUser(next);

      clearVerifyTimer();
      verifyTimerRef.current = window.setTimeout(() => {
        verifyTimerRef.current = null;
        const u = readUser();
        if (!u) return;
        const verified = { ...u, kycStatus: "verified" as KycStatus };
        persistUser(verified);
        setUser(verified);
      }, KYC_VERIFY_DELAY_MS);
    },
    [clearVerifyTimer],
  );

  const value = useMemo<AuthContextValue>(
    () => ({
      user,
      hydrated,
      kycProfile,
      signIn,
      signUp,
      verifyTwoFactor,
      completeEmailVerification,
      signOut,
      updateProfile,
      refreshAccountStatus,
      setKycStatus,
      saveKycProfile,
    }),
    [
      user,
      hydrated,
      kycProfile,
      signIn,
      signUp,
      verifyTwoFactor,
      completeEmailVerification,
      signOut,
      updateProfile,
      refreshAccountStatus,
      setKycStatus,
      saveKycProfile,
    ],
  );

  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}

export function useAuth() {
  const ctx = useContext(AuthContext);
  if (!ctx) throw new Error("useAuth must be used within AuthProvider");
  return ctx;
}
