import { Injectable, signal } from '@angular/core';
import { environment } from '../../environments/environment';
import { generateCodeChallenge, generateRandomString } from './pkce';

const ACCESS_TOKEN_KEY = 'auth.accessToken';
const EXPIRES_AT_KEY = 'auth.expiresAt';
const CODE_VERIFIER_KEY = 'auth.codeVerifier';
const STATE_KEY = 'auth.state';

interface DiscoveryDocument {
  authorization_endpoint: string;
}

interface SessionResponse {
  accessToken: string;
  expiresIn: number;
}

/**
 * The IdP client backing this app is confidential (holds a client secret), so
 * the authorization-code-for-tokens exchange must happen server-side — see
 * `POST /api/v1/auth/session`. This service only performs the browser-side
 * Authorization Code + PKCE redirect and hands the resulting code + PKCE
 * verifier to the backend; it never sees or stores the client secret.
 */
@Injectable({ providedIn: 'root' })
export class AuthService {
  private discoveryPromise: Promise<DiscoveryDocument> | undefined;

  readonly isAuthenticated = signal(this.hasValidAccessToken());

  private hasValidAccessToken(): boolean {
    const expiresAt = Number(sessionStorage.getItem(EXPIRES_AT_KEY) ?? 0);
    return !!sessionStorage.getItem(ACCESS_TOKEN_KEY) && Date.now() < expiresAt;
  }

  private discover(): Promise<DiscoveryDocument> {
    if (!this.discoveryPromise) {
      const issuer = environment.oidc.issuer.replace(/\/$/, '');
      this.discoveryPromise = fetch(`${issuer}/.well-known/openid-configuration`).then((response) => response.json());
    }
    return this.discoveryPromise;
  }

  async login(): Promise<void> {
    const codeVerifier = generateRandomString();
    const state = generateRandomString();
    const codeChallenge = await generateCodeChallenge(codeVerifier);
    sessionStorage.setItem(CODE_VERIFIER_KEY, codeVerifier);
    sessionStorage.setItem(STATE_KEY, state);

    const discovery = await this.discover();
    const url = new URL(discovery.authorization_endpoint);
    url.searchParams.set('response_type', 'code');
    url.searchParams.set('client_id', environment.oidc.clientId);
    url.searchParams.set('redirect_uri', environment.oidc.redirectUri);
    url.searchParams.set('scope', environment.oidc.scope);
    url.searchParams.set('state', state);
    url.searchParams.set('code_challenge', codeChallenge);
    url.searchParams.set('code_challenge_method', 'S256');

    window.location.href = url.toString();
  }

  async completeLogin(): Promise<void> {
    const params = new URLSearchParams(window.location.search);
    const code = params.get('code');
    const state = params.get('state');
    const codeVerifier = sessionStorage.getItem(CODE_VERIFIER_KEY);
    const expectedState = sessionStorage.getItem(STATE_KEY);

    if (!code || !state || !codeVerifier || state !== expectedState) {
      throw new Error('Invalid or missing OIDC callback parameters');
    }

    const response = await fetch(`${environment.apiBaseUrl}/auth/session`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ code, codeVerifier, redirectUri: environment.oidc.redirectUri }),
    });

    if (!response.ok) {
      throw new Error('Failed to exchange the authorization code for a session');
    }

    const session = (await response.json()) as SessionResponse;
    sessionStorage.setItem(ACCESS_TOKEN_KEY, session.accessToken);
    sessionStorage.setItem(EXPIRES_AT_KEY, String(Date.now() + session.expiresIn * 1000));
    sessionStorage.removeItem(CODE_VERIFIER_KEY);
    sessionStorage.removeItem(STATE_KEY);
    this.isAuthenticated.set(true);
  }

  logout(): void {
    sessionStorage.removeItem(ACCESS_TOKEN_KEY);
    sessionStorage.removeItem(EXPIRES_AT_KEY);
    this.isAuthenticated.set(false);
  }

  async getAccessToken(): Promise<string | undefined> {
    return this.hasValidAccessToken() ? (sessionStorage.getItem(ACCESS_TOKEN_KEY) ?? undefined) : undefined;
  }
}
