import { DOCUMENT } from '@angular/common';
import { HttpErrorResponse, HttpResponse } from '@angular/common/http';
import { isPlatformBrowser, isPlatformServer } from '@angular/common';
import { computed, inject, Injectable, PLATFORM_ID, signal, TransferState, makeStateKey } from '@angular/core';
import { firstValueFrom, map, Observable, of, tap, timeout } from 'rxjs';

import { environment } from '../../../../environments/environment';
import { BaseApiService } from '../base-api.service';
import {
  AuthUser,
  LoginPayload,
  LoginResponse,
  RegisterPayload,
  RegisterResponse,
  ResetPasswordPayload,
  ResetPasswordAttemptResponse,
  ResetPasswordResponse,
  UpdateProfilePayload,
  ValidateResetPasswordAttemptResponse
} from './auth.interfaces';
import { CookieService } from '../cookie/cookie.service';
import { TenantService } from '../tenant.service';
import { BOOTSTRAP_REQUEST_TIMEOUT_MS } from '../bootstrap-request-timeout';

const AUTH_TOKEN_COOKIE_KEY = 'shopit.front.auth.token';
const AUTH_USER_SSR_STATE_KEY = makeStateKey<AuthUser>('shopit.auth.user');

@Injectable({
  providedIn: 'root'
})
export class AuthService extends BaseApiService {
  private readonly document = inject(DOCUMENT);
  private readonly platformId = inject(PLATFORM_ID);
  private readonly cookieService = inject(CookieService);
  private readonly transferState = inject(TransferState);
  private readonly tenantService = inject(TenantService);
  private readonly bootstrapRequestTimeoutMs = inject(BOOTSTRAP_REQUEST_TIMEOUT_MS);

  private readonly userState = signal<AuthUser | null>(null);
  private readonly tokenState = signal<string | null>(null);

  readonly user = this.userState.asReadonly();
  readonly token = this.tokenState.asReadonly();
  readonly isAuthenticated = computed(() => this.tokenState() !== null);

  login(payload: LoginPayload): Observable<AuthUser> {
    const tenant = this.tenantService.getTenant();
    if (!tenant) {
      throw new Error('No se pudo resolver el tenant activo.');
    }

    return this.http
      .post<LoginResponse>(
        `${environment.url}login`,
        { ...payload, tenant_codigo: tenant.codigo },
        {
          withCredentials: true
        }
      )
      .pipe(
        tap((response) => this.applyAuthenticatedState(response.token, response.user)),
        map((response) => response.user)
      );
  }

  register(payload: RegisterPayload): Observable<RegisterResponse> {
    const tenantCode = this.tenantService.getTenant()?.codigo;

    return this.http.post<RegisterResponse>(
      `${environment.url}register`,
      {
        ...payload,
        ...(tenantCode ? { tenant_codigo: tenantCode } : {})
      },
    );
  }

  requestPasswordReset(
    email: string
  ): Observable<HttpResponse<ResetPasswordAttemptResponse>> {
    const tenantCode = this.tenantService.getTenant()?.codigo;

    return this.http.post<ResetPasswordAttemptResponse>(
      `${environment.url}password/reset-attempts`,
      {
        email,
        ...(tenantCode ? { tenant_codigo: tenantCode } : {})
      },
      {
        observe: 'response'
      }
    );
  }

  validatePasswordResetCode(
    email: string,
    codigo: string
  ): Observable<HttpResponse<ValidateResetPasswordAttemptResponse>> {
    return this.http.post<ValidateResetPasswordAttemptResponse>(
      `${environment.url}password/reset-attempts/validate`,
      { email, codigo },
      {
        observe: 'response'
      }
    );
  }

  resetPassword(payload: ResetPasswordPayload): Observable<HttpResponse<ResetPasswordResponse>> {
    return this.http.post<ResetPasswordResponse>(`${environment.url}password/reset`, payload, {
      observe: 'response'
    });
  }

  loginWithGoogle(): void {
    const tenant = this.tenantService.getTenant();
    if (!tenant) {
      throw new Error('No se pudo resolver el tenant activo.');
    }

    const apiUrl = new URL(environment.url);
    const authorizationUrl = new URL('/auth/google/redirect', apiUrl.origin);
    const basePath = tenant.base_path && tenant.base_path !== '/' ? tenant.base_path : '';
    authorizationUrl.searchParams.set('tenant', tenant.codigo);
    authorizationUrl.searchParams.set(
      'return_url',
      `${this.document.location.origin}${basePath}`,
    );

    this.document.location.assign(authorizationUrl.toString());
  }

  completeGoogleLogin(oauthCode: string): Observable<AuthUser> {
    const tenant = this.tenantService.getTenant();
    if (!tenant) {
      throw new Error('No se pudo resolver el tenant activo.');
    }

    return this.http
      .post<LoginResponse>(
        `${environment.url}auth/google/exchange`,
        { oauth_code: oauthCode, tenant_codigo: tenant.codigo },
        {
          withCredentials: true
        }
      )
      .pipe(
        tap((response) => this.applyAuthenticatedState(response.token, response.user)),
        map((response) => response.user)
      );
  }

  updateProfile(payload: UpdateProfilePayload): Observable<AuthUser> {
    return this.http
      .put<AuthUser>(`${environment.url}me`, payload)
      .pipe(tap((user) => this.userState.set(user)));
  }

  logout(): Observable<void> {
    if (!this.tokenState()) {
      this.clearSession();
      return of(void 0);
    }

    return this.http
      .post<void>(`${environment.url}logout`, {})
      .pipe(tap(() => this.clearSession()));
  }

  async bootstrap(): Promise<void> {
    this.hydrateSession();

    if (!this.tokenState() || this.userState()) {
      return;
    }

    const transferredUser = this.transferState.get(AUTH_USER_SSR_STATE_KEY, null);
    if (transferredUser) {
      this.transferState.remove(AUTH_USER_SSR_STATE_KEY);
      this.userState.set(transferredUser);
      return;
    }

    try {
      const user = await firstValueFrom(
        this.loadCurrentUser().pipe(timeout(this.bootstrapRequestTimeoutMs)),
      );

      if (isPlatformServer(this.platformId)) {
        this.transferState.set(AUTH_USER_SSR_STATE_KEY, user);
      }
    } catch (error) {
      // An expired token is an expected state, including while rendering on the
      // server. Do not let it reject the application initializer: Angular's SSR
      // pipeline can otherwise attempt to serialize HttpErrorResponse internals
      // as HTTP headers.
      if (error instanceof HttpErrorResponse && error.status === 401) {
        this.clearSession();

        return;
      }

      throw error;
    }
  }

  loadCurrentUser(): Observable<AuthUser> {
    return this.http
      .get<AuthUser>(`${environment.url}me`)
      .pipe(tap((user) => this.userState.set(user)));
  }

  hydrateSession(): void {
    const token = this.cookieService.get(AUTH_TOKEN_COOKIE_KEY);
    this.tokenState.set(token);
  }

  clearSession(): void {
    this.userState.set(null);
    this.tokenState.set(null);
    this.cookieService.delete(AUTH_TOKEN_COOKIE_KEY);
  }

  private applyAuthenticatedState(token: string, user: AuthUser): void {
    this.tokenState.set(token);
    this.userState.set(user);
    this.cookieService.set(AUTH_TOKEN_COOKIE_KEY, token);
  }
}
