import { randomUUID } from "node:crypto";
import fs from "node:fs/promises";
import path from "node:path";
import type { OAuthRegisteredClientsStore } from "@modelcontextprotocol/sdk/server/auth/clients.js";
import type { OAuthClientInformationFull } from "@modelcontextprotocol/sdk/shared/auth.js";
import type { AppConfig } from "../config.js";

type ClientsFile = {
  version: 1;
  clients: OAuthClientInformationFull[];
};

export class PersistentClientStore implements OAuthRegisteredClientsStore {
  private queue: Promise<void> = Promise.resolve();
  private readonly staticClient?: OAuthClientInformationFull;

  constructor(
    private readonly filePath: string,
    private readonly allowedRedirectHosts: string[],
    private readonly allowDynamicRegistration: boolean,
    staticClient: AppConfig["oauth"]["staticClient"],
    private readonly allowLocalhost: boolean,
  ) {
    if (staticClient) {
      staticClient.redirectUris.forEach((uri) => this.assertRedirectUri(uri));
      this.staticClient = {
        client_id: staticClient.clientId,
        client_secret: staticClient.clientSecret,
        redirect_uris: staticClient.redirectUris,
        client_name: "ChatGPT predefined client",
        token_endpoint_auth_method: "client_secret_post",
        grant_types: ["authorization_code", "refresh_token"],
        response_types: ["code"],
      };
    }

  }

  async getClient(clientId: string): Promise<OAuthClientInformationFull | undefined> {
    if (this.staticClient?.client_id === clientId) return this.staticClient;
    const state = await this.readState();
    return state.clients.find((client) => client.client_id === clientId);
  }

  async registerClient(
    clientInput: Omit<OAuthClientInformationFull, "client_id" | "client_id_issued_at">,
  ): Promise<OAuthClientInformationFull> {
    if (!this.allowDynamicRegistration) {
      throw new Error("Dynamic client registration is disabled");
    }

    const supplied = clientInput as Partial<OAuthClientInformationFull>;
    const redirectUris = supplied.redirect_uris ?? [];
    if (redirectUris.length < 1 || redirectUris.length > 10) {
      throw new Error("OAuth clients must register between 1 and 10 redirect URIs");
    }
    redirectUris.forEach((uri) => this.assertRedirectUri(uri));

    const authMethod = supplied.token_endpoint_auth_method ?? "none";
    if (!["none", "client_secret_post"].includes(authMethod)) {
      throw new Error("Unsupported token endpoint authentication method");
    }

    const client: OAuthClientInformationFull = {
      ...clientInput,
      client_id: supplied.client_id ?? randomUUID(),
      client_id_issued_at: supplied.client_id_issued_at ?? Math.floor(Date.now() / 1000),
      redirect_uris: redirectUris,
      token_endpoint_auth_method: authMethod,
      grant_types: supplied.grant_types ?? ["authorization_code", "refresh_token"],
      response_types: supplied.response_types ?? ["code"],
    };

    if (!client.grant_types?.includes("authorization_code") || !client.response_types?.includes("code")) {
      throw new Error("Only the OAuth authorization-code flow is supported");
    }

    return this.runExclusive(async () => {
      const state = await this.readState();
      const existing = state.clients.find((item) => item.client_id === client.client_id);
      if (existing) return existing;
      if (state.clients.length >= 50) {
        throw new Error("Dynamic OAuth client limit reached");
      }
      state.clients.push(client);
      await this.writeState(state);
      return client;
    });
  }

  private assertRedirectUri(uri: string): void {
    const parsed = new URL(uri);
    const isLoopback = ["localhost", "127.0.0.1", "::1"].includes(parsed.hostname);
    if (parsed.protocol !== "https:" && !(this.allowLocalhost && isLoopback)) {
      throw new Error("OAuth redirect URIs must use HTTPS");
    }
    if (isLoopback && this.allowLocalhost) return;

    const hostname = parsed.hostname.toLowerCase();
    const allowed = this.allowedRedirectHosts.some(
      (candidate) => hostname === candidate || hostname.endsWith(`.${candidate}`),
    );
    if (!allowed) {
      throw new Error(`OAuth redirect host is not allowed: ${hostname}`);
    }
  }

  private async readState(): Promise<ClientsFile> {
    try {
      const raw = await fs.readFile(this.filePath, "utf8");
      const parsed = JSON.parse(raw) as ClientsFile;
      if (parsed.version !== 1 || !Array.isArray(parsed.clients)) {
        throw new Error("Unsupported OAuth clients state format");
      }
      return parsed;
    } catch (error) {
      if ((error as NodeJS.ErrnoException).code === "ENOENT") {
        return { version: 1, clients: [] };
      }
      throw error;
    }
  }

  private async writeState(state: ClientsFile): Promise<void> {
    const directory = path.dirname(this.filePath);
    await fs.mkdir(directory, { recursive: true, mode: 0o700 });
    const temporary = `${this.filePath}.${randomUUID()}.tmp`;
    await fs.writeFile(temporary, `${JSON.stringify(state, null, 2)}\n`, { mode: 0o600 });
    await fs.rename(temporary, this.filePath);
  }

  private runExclusive<T>(operation: () => Promise<T>): Promise<T> {
    const result = this.queue.then(operation, operation);
    this.queue = result.then(
      () => undefined,
      () => undefined,
    );
    return result;
  }
}
