import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { BehaviorSubject, Observable, of } from 'rxjs';
import { catchError, filter, finalize, map, shareReplay, take, tap } from 'rxjs/operators';
import { ApiConfigService } from '../../core/services/api-config.service';
import { ModuleActions } from '../../core/models/permission.model';

// ── Public interfaces ─────────────────────────────────────────────────────────

/** Matches the C# MemberInfo DTO exactly. */
export interface MemberInfo {
    id: number;
    nickName: string | null;
    firstName_en: string;
    lastName_en: string;
    firstName_zh: string | null;
    lastName_zh: string | null;
}

/** Matches the C# UserInfo DTO exactly. */
export interface UserInfo {
    id: string;
    email: string;
    roles: string[];
    languagePreference: string;
    /**
     * Effective permissions, keyed by camelCased module name (server uses a
     * camelCase dictionary-key policy). Absent for legacy/secret-link tokens.
     */
    permissions?: Record<string, ModuleActions>;
    /**
     * The church member linked to this account, or absent for admin-only
     * accounts and accounts whose member record was deleted. Flows through
     * login, refresh, and /me so the greeting survives a page reload.
     */
    memberInfo?: MemberInfo;
}

/** Matches the C# LoginResponse DTO exactly. */
export interface ApiLoginResponse {
    accessToken: string;
    expiresIn: number;
    user: UserInfo;
}

export interface LoginCredentials {
    email: string;
    password: string;
    /** Reserved for future MFA support — ignored by the current API. */
    mfaCode?: string;
}

export enum LoginResultType {
    Success              = 'Success',
    /** Kept dormant — the current API has no MFA endpoint. */
    MfaRequired          = 'MfaRequired',
    InvalidCredentials   = 'InvalidCredentials',
    Error                = 'Error'
}

export interface LoginResult {
    result: LoginResultType;
    responseData?: UserInfo;
    message?: string;
}

/** Matches the C# ValidateInvitationResult DTO. */
export interface ValidateInvitationResult {
    valid: boolean;
    expired: boolean;
    memberName?: string;
    email?: string;
}

export interface TokenVerificationResult {
    isValid: boolean;
    /** Constructed from JWT claims when using secret-link login. */
    user?: UserInfo;
    /** The raw JWT from the URL — use as the access token for this session. */
    accessToken?: string;
    message?: string;
    expiresAt?: Date;
    requiresMfa?: boolean;
}

// ── Service ───────────────────────────────────────────────────────────────────

@Injectable({ providedIn: 'root' })
export class AuthService {

    /**
     * In-memory only — never written to localStorage.
     * Non-private intentionally: unit tests seed state via these subjects directly.
     * Production code must use getToken(), getCurrentUser(), and setCurrentUser().
     */
    accessToken$ = new BehaviorSubject<string | null>(null);
    currentUser$ = new BehaviorSubject<UserInfo | null>(null);

    /** Observable stream of the current user (null = not authenticated). */
    public currentUser = this.currentUser$.asObservable();

    private readonly sessionReady$ = new BehaviorSubject(false);
    private refreshInFlight$: Observable<boolean> | null = null;
    private redirectUrl = '/dashboard';

    constructor(
        private http: HttpClient,
        private apiConfig: ApiConfigService
    ) {}

    // ── Auth API calls ──────────────────────────────────────────────────────

    /**
     * Authenticate with email + password.
     * On success, stores the access token and user in memory and returns
     * LoginResultType.Success.  Never throws — errors are mapped to LoginResult.
     */
    login(credentials: LoginCredentials): Observable<LoginResult> {
        return this.http.post<ApiLoginResponse>(
            `${this.apiConfig.authUrl}/login`,
            { email: credentials.email, password: credentials.password },
            { withCredentials: true }
        ).pipe(
            tap(response => {
                this.accessToken$.next(response.accessToken);
                this.currentUser$.next(response.user);
            }),
            map(response => ({
                result: LoginResultType.Success,
                responseData: response.user
            } as LoginResult)),
            catchError(error => {
                if (error.status === 401) {
                    return of({
                        result: LoginResultType.InvalidCredentials,
                        message: error.error?.message || 'Invalid email or password'
                    } as LoginResult);
                }
                return of({
                    result: LoginResultType.Error,
                    message: error.error?.message || 'An error occurred during login'
                } as LoginResult);
            })
        );
    }

    /**
     * Silently exchange the HttpOnly `rolac_rt` cookie for a new access token.
     * Returns true on success, false if the cookie is absent or expired.
     * Never throws.
     */
    refresh(): Observable<boolean> {
        if (!this.refreshInFlight$) {
            this.refreshInFlight$ = this.http.post<ApiLoginResponse>(
                `${this.apiConfig.authUrl}/refresh`,
                {},
                { withCredentials: true }
            ).pipe(
                tap(response => {
                    this.accessToken$.next(response.accessToken);
                    this.currentUser$.next(response.user);
                }),
                map(() => true),
                catchError(() => of(false)),
                finalize(() => {
                    this.refreshInFlight$ = null;
                }),
                shareReplay(1)
            );
        }
        return this.refreshInFlight$;
    }

    /**
     * Changes the current user's password. Sends the cookie so the server can
     * keep the current session alive while revoking the user's other sessions.
     * Emits void on success (204); errors propagate so the caller can show the
     * server message.
     */
    changePassword(currentPassword: string, newPassword: string): Observable<void> {
        return this.http.post<void>(
            `${this.apiConfig.authUrl}/change-password`,
            { currentPassword, newPassword },
            { withCredentials: true }
        );
    }

    /**
     * Checks whether an invitation token is still usable (anonymous). Used by the
     * public "set your password" page to decide what to show before the member types.
     */
    validateInvitation(token: string): Observable<ValidateInvitationResult> {
        return this.http.get<ValidateInvitationResult>(
            `${this.apiConfig.authUrl}/invitation/validate`,
            { params: { token } }
        );
    }

    /**
     * Consumes an invitation: sets the password and logs the member in. On success the
     * server returns a normal login payload, so we store the access token + user (and the
     * refresh cookie is set server-side) exactly like login(). Errors propagate to the caller.
     */
    acceptInvitation(token: string, newPassword: string): Observable<UserInfo> {
        return this.http.post<ApiLoginResponse>(
            `${this.apiConfig.authUrl}/accept-invitation`,
            { token, newPassword },
            { withCredentials: true }
        ).pipe(
            tap(response => {
                this.accessToken$.next(response.accessToken);
                this.currentUser$.next(response.user);
            }),
            map(response => response.user)
        );
    }

    /**
     * Clears in-memory auth state immediately, then fires a fire-and-forget
     * POST to revoke the server-side refresh token cookie.
     */
    logout(): void {
        this.accessToken$.next(null);
        this.currentUser$.next(null);
        this.http.post(
            `${this.apiConfig.authUrl}/logout`,
            {},
            { withCredentials: true }
        ).pipe(
            catchError(() => of(null))
        ).subscribe();
    }

    /**
     * Called by APP_INITIALIZER on every page load.
     * Attempts to restore the session via the refresh token cookie.
     * Always resolves — never rejects — so it cannot block app bootstrap.
     */
    initializeFromRefreshToken(): Promise<void> {
        return new Promise(resolve => {
            this.refresh().pipe(
                finalize(() => {
                    this.sessionReady$.next(true);
                    resolve();
                })
            ).subscribe();
        });
    }

    /** Resolves once startup session restore has finished (success or failure). */
    whenSessionReady(): Observable<boolean> {
        if (this.sessionReady$.value) {
            return of(true);
        }
        return this.sessionReady$.pipe(filter(Boolean), take(1));
    }

    // ── State accessors ─────────────────────────────────────────────────────

    getToken(): string | null {
        return this.accessToken$.value;
    }

    isAuthenticated(): boolean {
        return this.currentUser$.value !== null && this.getToken() !== null;
    }

    getCurrentUser(): UserInfo | null {
        return this.currentUser$.value;
    }

    /**
     * Manually set the current user — used by the MFA success callback
     * and the secret-link token flow.
     */
    setCurrentUser(user: UserInfo): void {
        this.currentUser$.next(user);
    }

    setRedirectUrl(url: string): void {
        this.redirectUrl = url;
    }

    getRedirectUrl(): string {
        return this.redirectUrl;
    }

    // ── Secret-link token helpers (unchanged logic, updated types) ───────────

    /**
     * Verifies a JWT token received as a URL parameter (secret-link login).
     * Performs local verification only — no API call.
     * Constructs a UserInfo from the JWT claims (id, email, roles,
     * languagePreference).
     */
    verifySecretLinkToken(token: string): Observable<TokenVerificationResult> {
        try {
            const tokenData = this.parseJwtToken(token);

            if (!tokenData) {
                return of({ isValid: false, message: 'Invalid token format' });
            }

            if (this.isTokenExpired(token)) {
                return of({
                    isValid: false,
                    message: 'This link has expired. Please request a new one.'
                });
            }

            const user: UserInfo = {
                id: tokenData.userId || tokenData.sub || tokenData.id || '',
                email: tokenData.email || tokenData.email_address || '',
                roles: Array.isArray(tokenData.roles)
                    ? tokenData.roles
                    : tokenData.role ? [tokenData.role] : [],
                languagePreference: tokenData.languagePreference || 'en'
            };

            return of({
                isValid: true,
                user,
                message: 'Token verified successfully. MFA required.',
                expiresAt: tokenData.exp ? new Date(tokenData.exp * 1000) : undefined,
                requiresMfa: true
            });

        } catch (error) {
            return of({ isValid: false, message: 'Invalid or corrupted token' });
        }
    }

    /** Returns true if the JWT's `exp` claim is in the past. */
    isTokenExpired(token: string): boolean {
        try {
            const payload = JSON.parse(atob(token.split('.')[1]));
            return payload.exp < Math.floor(Date.now() / 1000);
        } catch {
            return true;
        }
    }

    private parseJwtToken(token: string): any | null {
        try {
            const parts = token.split('.');
            if (parts.length !== 3) return null;
            const decoded = atob(parts[1].replace(/-/g, '+').replace(/_/g, '/'));
            return JSON.parse(decoded);
        } catch {
            return null;
        }
    }
}
