import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
import type { ReactNode } from 'react';

export type Role = 'USER' | 'ADMIN';
export type AuthProviderName = 'LOCAL' | 'GOOGLE' | 'FACEBOOK';
export type AccountType = 'PERSONAL' | 'COMPANY';
export type ContactPreference = 'EMAIL' | 'PHONE' | 'EMAIL_AND_PHONE';
export type PreferredLanguage = 'PL' | 'EN' | 'UK' | 'DE';

export type Currency = 'PLN' | 'EUR' | 'USD';
export type AreaUnit = 'M2' | 'FT2';
export type ProfileVisibility = 'PUBLIC' | 'CONTACTS' | 'PRIVATE';

/**
 * Ustawienia konta trzymane na serwerze (/api/me/settings). Zgody marketingowe celowo tu nie leza -
 * ich jedynym zrodlem jest /api/me/marketing, bo steruja kwalifikacja do kampanii.
 */
export type UserSettings = {
  bio: string | null;
  profileVisibility: ProfileVisibility;
  avatarImage: string | null;
  coverImage: string | null;
  currency: Currency;
  areaUnit: AreaUnit;
  directOffersOnly: boolean;
  hideInactiveOffers: boolean;
  saveSearchesOnHome: boolean;
  notifySavedSearches: boolean;
  notifyPriceAlerts: boolean;
  notifyMessages: boolean;
  notifyProductNews: boolean;
  searchLocations: string | null;
  searchPropertyType: string | null;
  searchBudgetMax: number | null;
  searchAreaMin: number | null;
  searchAreaMax: number | null;
  searchRoomsMin: number | null;
  searchRoomsMax: number | null;
};

// Zapis czesciowy: pominiete pole zostaje bez zmian, pusty tekst je czysci, liczba ujemna zeruje.
export type UserSettingsPatch = Partial<Record<keyof UserSettings, unknown>>;

export type AuthUser = {
  id: number;
  email: string;
  fullName: string;
  nick: string | null;
  role: Role;
  provider: AuthProviderName;
  accountType: AccountType;
  phone: string | null;
  address: string | null;
  contactPreference: ContactPreference;
  preferredLanguage: PreferredLanguage;
  nip: string | null;
  birthDate: string | null;
  verified: boolean;
  phoneVerified: boolean;
  blocked: boolean;
  promotionCredits: number;
  createdAt: string;
};

type AuthResponse = { token: string; user: AuthUser };

// Po rejestracji konto wymaga aktywacji linkiem e-mail - nie logujemy od razu.
export type RegisterPending = { email: string; phoneVerificationRequired: boolean };

export type RegisterDetails = {
  accountType: AccountType;
  phone?: string;
  nip?: string;
  address?: string;
};

type AuthContextValue = {
  user: AuthUser | null;
  token: string | null;
  loading: boolean;
  login: (email: string, password: string) => Promise<AuthUser>;
  register: (email: string, password: string, fullName: string, details?: RegisterDetails) => Promise<RegisterPending>;
  socialLogin: (provider: Exclude<AuthProviderName, 'LOCAL'>) => Promise<AuthUser>;
  activateAccount: (token: string) => Promise<void>;
  resendActivation: (email: string) => Promise<void>;
  forgotPassword: (email: string) => Promise<void>;
  resetPassword: (token: string, password: string) => Promise<void>;
  verifyPhone: (email: string, code: string) => Promise<void>;
  resendPhoneOtp: (email: string) => Promise<void>;
  updateProfile: (profile: ProfileUpdate) => Promise<AuthUser>;
  changePassword: (currentPassword: string, newPassword: string) => Promise<void>;
  deleteAccount: (password: string) => Promise<void>;
  loadSettings: () => Promise<UserSettings>;
  saveSettings: (patch: UserSettingsPatch) => Promise<UserSettings>;
  refreshUser: () => Promise<AuthUser | null>;
  logout: () => void;
};

/**
 * PUT /auth/me nadpisuje caly profil, wiec wysylamy komplet pol, a nie tylko zmienione.
 * Pusty nick oznacza "zostaw dotychczasowy" - backend nie zmienia wtedy nazwy uzytkownika.
 */
export type ProfileUpdate = {
  fullName: string;
  nick?: string;
  phone?: string;
  birthDate?: string;
  address?: string;
  contactPreference?: ContactPreference;
  preferredLanguage?: PreferredLanguage;
};

const TOKEN_KEY = 'polskalokalnie-auth-token';
const API_BASE = '/api';

export function getToken(): string | null {
  return window.localStorage.getItem(TOKEN_KEY);
}

/**
 * Cienki wrapper na fetch: dokleja token Bearer i zamienia bledy backendu (pole "message")
 * na wyjatek z czytelnym komunikatem po polsku.
 */
export async function apiFetch<T>(path: string, options: RequestInit = {}): Promise<T> {
  const token = getToken();
  const headers = new Headers(options.headers);
  if (options.body && !headers.has('Content-Type')) {
    headers.set('Content-Type', 'application/json');
  }
  if (token) {
    headers.set('Authorization', `Bearer ${token}`);
  }

  const response = await fetch(`${API_BASE}${path}`, { ...options, headers });

  if (!response.ok) {
    let message = 'Wystąpił błąd. Spróbuj ponownie.';
    try {
      const data = await response.json();
      if (data && typeof data.message === 'string' && data.message.trim()) {
        message = data.message;
      }
    } catch {
      // brak/niepoprawne cialo JSON - zostaje komunikat domyslny
    }
    const error = new Error(message) as Error & { status?: number };
    error.status = response.status;
    throw error;
  }

  if (response.status === 204) {
    return undefined as T;
  }
  return (await response.json()) as T;
}

const AuthContext = createContext<AuthContextValue | null>(null);

export function AuthProvider({ children }: { children: ReactNode }) {
  const [token, setToken] = useState<string | null>(() => getToken());
  const [user, setUser] = useState<AuthUser | null>(null);
  const [loading, setLoading] = useState<boolean>(() => Boolean(getToken()));

  // Po odswiezeniu strony: majac token w localStorage, pobierz aktualny profil.
  useEffect(() => {
    if (!token) {
      setUser(null);
      setLoading(false);
      return;
    }
    let cancelled = false;
    setLoading(true);

    // Sesje konczymy wylacznie wtedy, gdy backend odrzuci token (401/403).
    // Chwilowy blad sieci albo 5xx nie moze wylogowywac uzytkownika - taki blad
    // ponawiamy raz, a token zostaje, wiec kolejne wejscie na strone go odzyska.
    const loadProfile = async (canRetry: boolean): Promise<void> => {
      try {
        const me = await apiFetch<AuthUser>('/auth/me');
        if (!cancelled) {
          setUser(me);
          setLoading(false);
        }
      } catch (error) {
        if (cancelled) {
          return;
        }
        const status = (error as { status?: number }).status;
        if (status === 401 || status === 403) {
          window.localStorage.removeItem(TOKEN_KEY);
          setToken(null);
          setUser(null);
          setLoading(false);
          return;
        }
        if (canRetry) {
          await new Promise((resolve) => { window.setTimeout(resolve, 400); });
          if (!cancelled) {
            await loadProfile(false);
          }
          return;
        }
        setUser(null);
        setLoading(false);
      }
    };

    void loadProfile(true);
    return () => {
      cancelled = true;
    };
  }, [token]);

  const applyAuth = useCallback((auth: AuthResponse) => {
    window.localStorage.setItem(TOKEN_KEY, auth.token);
    setToken(auth.token);
    setUser(auth.user);
    return auth.user;
  }, []);

  const login = useCallback(
    async (email: string, password: string) =>
      applyAuth(await apiFetch<AuthResponse>('/auth/login', {
        method: 'POST',
        body: JSON.stringify({ email, password }),
      })),
    [applyAuth],
  );

  // Rejestracja NIE loguje od razu - konto wymaga aktywacji linkiem z e-maila.
  const register = useCallback(
    async (email: string, password: string, fullName: string, details?: RegisterDetails) =>
      apiFetch<RegisterPending>('/auth/register', {
        method: 'POST',
        body: JSON.stringify({ email, password, fullName, ...details }),
      }),
    [],
  );

  const socialLogin = useCallback(
    async (provider: Exclude<AuthProviderName, 'LOCAL'>) =>
      applyAuth(await apiFetch<AuthResponse>('/auth/social', {
        method: 'POST',
        body: JSON.stringify({ provider }),
      })),
    [applyAuth],
  );

  const activateAccount = useCallback(async (token: string) => {
    await apiFetch<void>('/auth/activate', { method: 'POST', body: JSON.stringify({ token }) });
  }, []);

  const resendActivation = useCallback(async (email: string) => {
    await apiFetch<void>('/auth/resend-activation', { method: 'POST', body: JSON.stringify({ email }) });
  }, []);

  const forgotPassword = useCallback(async (email: string) => {
    await apiFetch<void>('/auth/forgot-password', { method: 'POST', body: JSON.stringify({ email }) });
  }, []);

  const resetPassword = useCallback(async (token: string, password: string) => {
    await apiFetch<void>('/auth/reset-password', { method: 'POST', body: JSON.stringify({ token, password }) });
  }, []);

  const verifyPhone = useCallback(async (email: string, code: string) => {
    await apiFetch<void>('/auth/verify-phone', { method: 'POST', body: JSON.stringify({ email, code }) });
  }, []);

  const resendPhoneOtp = useCallback(async (email: string) => {
    await apiFetch<void>('/auth/resend-phone-otp', { method: 'POST', body: JSON.stringify({ email }) });
  }, []);

  const updateProfile = useCallback(async (profile: ProfileUpdate) => {
    const updated = await apiFetch<AuthUser>('/auth/me', {
      method: 'PUT',
      body: JSON.stringify(profile),
    });
    setUser(updated);
    return updated;
  }, []);

  const changePassword = useCallback(async (currentPassword: string, newPassword: string) => {
    await apiFetch<void>('/auth/change-password', {
      method: 'POST',
      body: JSON.stringify({ currentPassword, newPassword }),
    });
  }, []);

  // Po usunieciu konta token jest bezuzyteczny - czyscimy sesje od razu, bez czekania na 401.
  const deleteAccount = useCallback(async (password: string) => {
    await apiFetch<void>('/auth/me', {
      method: 'DELETE',
      body: JSON.stringify({ password }),
    });
    window.localStorage.removeItem(TOKEN_KEY);
    setToken(null);
    setUser(null);
  }, []);

  const loadSettings = useCallback(async () => apiFetch<UserSettings>('/me/settings'), []);

  const saveSettings = useCallback(
    async (patch: UserSettingsPatch) =>
      apiFetch<UserSettings>('/me/settings', { method: 'PUT', body: JSON.stringify(patch) }),
    [],
  );

  // Ponowne pobranie danych uzytkownika (np. po zakupie promowania - odswieza saldo kredytow).
  const refreshUser = useCallback(async () => {
    if (!getToken()) {
      return null;
    }
    try {
      const fresh = await apiFetch<AuthUser>('/auth/me');
      setUser(fresh);
      return fresh;
    } catch {
      return null;
    }
  }, []);

  const logout = useCallback(() => {
    window.localStorage.removeItem(TOKEN_KEY);
    setToken(null);
    setUser(null);
  }, []);

  const value = useMemo<AuthContextValue>(
    () => ({
      user, token, loading, login, register, socialLogin,
      activateAccount, resendActivation, forgotPassword, resetPassword, verifyPhone, resendPhoneOtp,
      updateProfile, changePassword, deleteAccount, loadSettings, saveSettings, refreshUser, logout,
    }),
    [user, token, loading, login, register, socialLogin,
      activateAccount, resendActivation, forgotPassword, resetPassword, verifyPhone, resendPhoneOtp,
      updateProfile, changePassword, deleteAccount, loadSettings, saveSettings, refreshUser, logout],
  );

  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}

export function useAuth(): AuthContextValue {
  const ctx = useContext(AuthContext);
  if (!ctx) {
    throw new Error('useAuth must be used within an AuthProvider');
  }
  return ctx;
}
