import {
  InteractionRequiredAuthError,
  PublicClientApplication,
  type AccountInfo,
  type DeviceCodeRequest,
} from "@azure/msal-node";
import { MicrosoftAuthenticator, MojangClient, type MicrosoftMinecraftProfile } from "@xmcl/user";
import type { LauncherConfig } from "./config.js";
import type { LauncherLogger } from "./logger.js";
import { SecureMsalCachePlugin } from "./secure-cache.js";

export interface DeviceCodeNotice {
  userCode: string;
  verificationUri: string;
  expiresAt: string;
  message: string;
}

export interface LauncherAccount {
  id: string;
  name: string;
}

export interface MinecraftSession {
  accessToken: string;
  profile: MicrosoftMinecraftProfile;
}

export class AuthConfigurationError extends Error {
  override readonly name = "AuthConfigurationError";
}

export class MinecraftOwnershipError extends Error {
  override readonly name = "MinecraftOwnershipError";
}

export class LauncherAuth {
  private readonly cache: SecureMsalCachePlugin;
  private app: PublicClientApplication | undefined;
  private activeDeviceRequest: DeviceCodeRequest | undefined;
  private selectedAccount: AccountInfo | undefined;
  private cachedProfile: MicrosoftMinecraftProfile | undefined;

  constructor(
    private readonly config: LauncherConfig,
    cachePath: string,
    private readonly logger: LauncherLogger,
  ) {
    this.cache = new SecureMsalCachePlugin(cachePath, logger);
    this.app = this.createApplication(config.microsoft.clientId);
  }

  private createApplication(clientId: string): PublicClientApplication | undefined {
    return clientId === "" ? undefined : new PublicClientApplication({
      auth: {
        clientId,
        authority: this.config.microsoft.authority,
      },
      cache: { cachePlugin: this.cache },
      system: {
        loggerOptions: {
          piiLoggingEnabled: false,
          loggerCallback: (_level, message, containsPii) => {
            if (!containsPii) void this.logger.info(`MSAL: ${message}`);
          },
        },
      },
    });
  }

  get configured(): boolean { return this.app !== undefined; }

  async configure(clientId: string): Promise<void> {
    this.cancelSignIn();
    this.selectedAccount = undefined;
    this.cachedProfile = undefined;
    await this.cache.clear();
    this.config.microsoft.clientId = clientId;
    this.app = this.createApplication(clientId);
  }

  async restore(): Promise<LauncherAccount | undefined> {
    if (!this.app) return undefined;
    const accounts = await this.app.getAllAccounts();
    this.selectedAccount = accounts[0];
    if (!this.selectedAccount) return undefined;
    try {
      const session = await this.acquireMinecraftSession();
      return { id: session.profile.id, name: session.profile.name };
    } catch (error) {
      if (error instanceof InteractionRequiredAuthError) return undefined;
      await this.logger.warn("La session Microsoft enregistrée n'a pas pu être restaurée", error);
      return undefined;
    }
  }

  async signIn(onCode: (notice: DeviceCodeNotice) => void): Promise<LauncherAccount> {
    if (!this.app) {
      throw new AuthConfigurationError(
        "L'identifiant d'application Microsoft BG LANDS manque. Configure BGLANDS_MICROSOFT_CLIENT_ID à la compilation.",
      );
    }
    if (this.activeDeviceRequest) throw new Error("Une connexion Microsoft est déjà en cours");
    const request: DeviceCodeRequest = {
      scopes: this.config.microsoft.scopes,
      timeout: 900,
      deviceCodeCallback: (response) => onCode({
        userCode: response.userCode,
        verificationUri: response.verificationUri,
        expiresAt: new Date(Date.now() + response.expiresIn * 1_000).toISOString(),
        message: response.message,
      }),
    };
    this.activeDeviceRequest = request;
    try {
      const result = await this.app.acquireTokenByDeviceCode(request);
      if (!result?.account || !result.accessToken) throw new Error("Connexion Microsoft annulée ou incomplète");
      this.selectedAccount = result.account;
      this.cachedProfile = undefined;
      const session = await this.exchangeForMinecraft(result.accessToken);
      return { id: session.profile.id, name: session.profile.name };
    } finally {
      this.activeDeviceRequest = undefined;
    }
  }

  cancelSignIn(): void {
    if (this.activeDeviceRequest) this.activeDeviceRequest.cancel = true;
  }

  async signOut(): Promise<void> {
    this.cancelSignIn();
    if (this.app && this.selectedAccount) await this.app.signOut({ account: this.selectedAccount });
    this.selectedAccount = undefined;
    this.cachedProfile = undefined;
    await this.cache.clear();
  }

  async acquireMinecraftSession(): Promise<MinecraftSession> {
    if (!this.app) throw new AuthConfigurationError("Connexion Microsoft non configurée");
    if (!this.selectedAccount) {
      const accounts = await this.app.getAllAccounts();
      this.selectedAccount = accounts[0];
    }
    if (!this.selectedAccount) throw new InteractionRequiredAuthError("login_required", "Connecte-toi à Microsoft");
    const result = await this.app.acquireTokenSilent({
      account: this.selectedAccount,
      scopes: this.config.microsoft.scopes,
    });
    return this.exchangeForMinecraft(result.accessToken);
  }

  private async exchangeForMinecraft(microsoftAccessToken: string): Promise<MinecraftSession> {
    const authenticator = new MicrosoftAuthenticator();
    const { minecraftXstsResponse } = await authenticator.acquireXBoxToken(microsoftAccessToken);
    const claim = minecraftXstsResponse.DisplayClaims.xui[0];
    if (!claim) throw new Error("Le compte Xbox ne contient aucune identité joueur");
    const minecraft = await authenticator.loginMinecraftWithXBox(claim.uhs, minecraftXstsResponse.Token);
    const client = new MojangClient();
    const ownership = await client.checkGameOwnership(minecraft.access_token);
    if (ownership.items.length === 0) {
      throw new MinecraftOwnershipError("Ce compte Microsoft ne possède pas Minecraft: Java Edition");
    }
    const profile = await client.getProfile(minecraft.access_token);
    this.cachedProfile = profile;
    return { accessToken: minecraft.access_token, profile };
  }
}
