import type { BotConfig } from "../config.ts";
import type { SessionPayload } from "../types.ts";
import { SessionVault } from "../session/SessionVault.ts";

type JsonObject = Record<string, unknown>;
type TokenResponse = {
  access_token?: unknown;
  refresh_token?: unknown;
  token_type?: unknown;
  expires_in?: unknown;
  scope?: unknown;
};

const API_BASE = "https://api.kick.com";
const OAUTH_BASE = "https://id.kick.com";

export class KickOAuthClient {
  private readonly config: BotConfig;
  private readonly vault: SessionVault;
  private payload: SessionPayload | null = null;
  private refreshPromise: Promise<void> | null = null;
  private appToken: { value: string; expiresAt: number } | null = null;

  constructor(config: BotConfig, vault: SessionVault) {
    this.config = config;
    this.vault = vault;
  }

  async initialize(): Promise<void> {
    this.payload = await this.vault.load();
  }

  async exchangeAuthorizationCode(code: string, codeVerifier: string): Promise<SessionPayload> {
    const token = await this.tokenRequest({
      grant_type: "authorization_code",
      client_id: this.config.kickClientId,
      client_secret: this.config.kickClientSecret,
      redirect_uri: this.config.kickRedirectUri,
      code_verifier: codeVerifier,
      code
    });
    this.payload = this.payloadFromToken(token);
    const identity = await this.identity();
    if (identity.username.toLocaleLowerCase("fr") !== this.config.botUsername.toLocaleLowerCase("fr")) {
      this.payload = null;
      throw new Error(`Le compte autorisé est ${identity.username || "inconnu"}, pas ${this.config.botUsername}.`);
    }
    this.payload.kickUserId = identity.userId;
    this.payload.lastValidatedAt = new Date().toISOString();
    await this.vault.save(this.payload);
    return this.payload;
  }

  async identity(): Promise<{ username: string; userId: string }> {
    const response = await this.userRequest("/public/v1/users");
    const users = Array.isArray(response.data) ? response.data : [];
    const user = (users[0] ?? {}) as JsonObject;
    return {
      username: String(user.name ?? ""),
      userId: String(user.user_id ?? "")
    };
  }

  async userRequest(path: string, init: RequestInit = {}): Promise<JsonObject> {
    return this.authorizedRequest(`${API_BASE}${path}`, init, "user");
  }

  async appRequest(path: string, init: RequestInit = {}): Promise<JsonObject> {
    return this.authorizedRequest(`${API_BASE}${path}`, init, "app");
  }

  get session(): SessionPayload | null {
    return this.payload;
  }

  private async authorizedRequest(url: string, init: RequestInit, tokenKind: "user" | "app"): Promise<JsonObject> {
    for (let attempt = 0; attempt < 2; attempt++) {
      const token = tokenKind === "user"
        ? await this.userAccessToken(attempt > 0)
        : await this.appAccessToken(attempt > 0);
      const headers = new Headers(init.headers);
      headers.set("Accept", "application/json");
      headers.set("Authorization", `Bearer ${token}`);
      const response = await fetch(url, { ...init, headers });
      const payload = await response.json().catch(() => ({})) as JsonObject;
      if (response.status === 401 && attempt === 0) continue;
      if (!response.ok) {
        const message = String(payload.message ?? payload.error ?? `Kick HTTP ${response.status}`);
        throw new Error(`API Kick : ${message}`);
      }
      return payload;
    }
    throw new Error("Autorisation Kick refusée.");
  }

  private async userAccessToken(forceRefresh = false): Promise<string> {
    if (!this.payload) throw new Error("Autorisation OAuth FiIyx absente.");
    const expiresSoon = Date.parse(this.payload.expiresAt) <= Date.now() + 60_000;
    if (forceRefresh || expiresSoon) await this.refreshUserToken();
    if (!this.payload?.accessToken) throw new Error("Jeton OAuth FiIyx absent.");
    return this.payload.accessToken;
  }

  private async refreshUserToken(): Promise<void> {
    if (this.refreshPromise) return this.refreshPromise;
    this.refreshPromise = (async () => {
      if (!this.payload?.refreshToken) throw new Error("Jeton de renouvellement Kick absent.");
      const token = await this.tokenRequest({
        grant_type: "refresh_token",
        client_id: this.config.kickClientId,
        client_secret: this.config.kickClientSecret,
        refresh_token: this.payload.refreshToken
      });
      this.payload = this.payloadFromToken(token, this.payload);
      await this.vault.save(this.payload);
    })();
    try {
      await this.refreshPromise;
    } finally {
      this.refreshPromise = null;
    }
  }

  private async appAccessToken(forceRefresh = false): Promise<string> {
    if (!forceRefresh && this.appToken && this.appToken.expiresAt > Date.now() + 60_000) {
      return this.appToken.value;
    }
    const token = await this.tokenRequest({
      grant_type: "client_credentials",
      client_id: this.config.kickClientId,
      client_secret: this.config.kickClientSecret
    });
    const value = String(token.access_token ?? "");
    if (!value) throw new Error("Kick n’a pas fourni de jeton d’application.");
    this.appToken = {
      value,
      expiresAt: Date.now() + Math.max(60, Number(token.expires_in) || 3_600) * 1_000
    };
    return value;
  }

  private async tokenRequest(values: Record<string, string>): Promise<TokenResponse> {
    const response = await fetch(`${OAUTH_BASE}/oauth/token`, {
      method: "POST",
      headers: {
        Accept: "application/json",
        "Content-Type": "application/x-www-form-urlencoded"
      },
      body: new URLSearchParams(values)
    });
    const payload = await response.json().catch(() => ({})) as TokenResponse & { error?: unknown; message?: unknown };
    if (!response.ok) {
      throw new Error(`OAuth Kick : ${String(payload.message ?? payload.error ?? `HTTP ${response.status}`)}`);
    }
    return payload;
  }

  private payloadFromToken(token: TokenResponse, previous?: SessionPayload): SessionPayload {
    const accessToken = String(token.access_token ?? "");
    const refreshToken = String(token.refresh_token ?? previous?.refreshToken ?? "");
    if (!accessToken || !refreshToken) throw new Error("Réponse OAuth Kick incomplète.");
    const expiresIn = Math.max(60, Number(token.expires_in) || 3_600);
    const scopeText = String(token.scope ?? previous?.scopes.join(" ") ?? "");
    return {
      version: 2,
      provider: "kick",
      accountUsername: "fiIyx",
      kickUserId: previous?.kickUserId ?? "",
      accessToken,
      refreshToken,
      tokenType: "Bearer",
      scopes: scopeText.split(/\s+/).filter(Boolean),
      expiresAt: new Date(Date.now() + expiresIn * 1_000).toISOString(),
      lastValidatedAt: new Date().toISOString()
    };
  }
}
