import { createHmac, timingSafeEqual } from "node:crypto";

export type Pagination = {
  limit: number;
  offset: number;
  count: number;
  total: number | null;
  has_more: boolean;
};

export type Page<T> = { data: T[]; pagination: Pagination };
export type Encounter = {
  id: string;
  external_encounter_id: string;
  status: string;
  encounter_type: string;
  created_at: string;
};
export type SoapNote = Record<string, unknown> & { id: string };

export class GelypticApiError extends Error {
  constructor(
    public readonly status: number,
    public readonly code: string,
    message: string,
    public readonly requestId: string | null,
  ) {
    super(message);
    this.name = "GelypticApiError";
  }
}

type ClientOptions = {
  apiKey: string;
  baseUrl?: string;
  fetchImpl?: typeof fetch;
};

export class GelypticMD {
  private readonly apiKey: string;
  private readonly baseUrl: string;
  private readonly fetchImpl: typeof fetch;

  constructor({ apiKey, baseUrl = "https://gelypticmd.com/api/public/v1", fetchImpl = fetch }: ClientOptions) {
    if (!apiKey) throw new Error("apiKey is required");
    this.apiKey = apiKey;
    this.baseUrl = baseUrl.replace(/\/$/, "");
    this.fetchImpl = fetchImpl;
  }

  private async request<T>(path: string, init: RequestInit = {}): Promise<T> {
    const response = await this.fetchImpl(`${this.baseUrl}${path}`, {
      ...init,
      headers: {
        Authorization: `Bearer ${this.apiKey}`,
        Accept: "application/json",
        ...init.headers,
      },
    });
    const requestId = response.headers.get("x-request-id");
    if (!response.ok) {
      const payload = await response.json().catch(() => null) as
        | { error?: { code?: string; message?: string; request_id?: string } }
        | null;
      throw new GelypticApiError(
        response.status,
        payload?.error?.code ?? "HTTP_ERROR",
        payload?.error?.message ?? `Request failed with status ${response.status}`,
        payload?.error?.request_id ?? requestId,
      );
    }
    return response.json() as Promise<T>;
  }

  listEncounters(params: { limit?: number; offset?: number } = {}) {
    const query = new URLSearchParams();
    if (params.limit !== undefined) query.set("limit", String(params.limit));
    if (params.offset !== undefined) query.set("offset", String(params.offset));
    const suffix = query.size ? `?${query}` : "";
    return this.request<Page<Encounter>>(`/clinical/encounters${suffix}`);
  }

  createEncounter(input: { external_encounter_id: string; encounter_type?: string }) {
    return this.request<Encounter>("/clinical/encounters", {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify(input),
    });
  }

  listNotes(params: { limit?: number; offset?: number } = {}) {
    const query = new URLSearchParams(Object.entries(params).map(([key, value]) => [key, String(value)]));
    return this.request<Page<SoapNote>>(`/clinical/notes${query.size ? `?${query}` : ""}`);
  }

  getNote(id: string) {
    return this.request<SoapNote>(`/clinical/notes/${encodeURIComponent(id)}`);
  }

  getFhirBundle(id: string) {
    return this.request<Record<string, unknown>>(`/clinical/notes/${encodeURIComponent(id)}/fhir`, {
      headers: { Accept: "application/fhir+json" },
    });
  }

  async getHl7Message(id: string) {
    const response = await this.fetchImpl(`${this.baseUrl}/clinical/notes/${encodeURIComponent(id)}/hl7`, {
      headers: { Authorization: `Bearer ${this.apiKey}`, Accept: "application/hl7-v2" },
    });
    if (!response.ok) {
      throw new GelypticApiError(response.status, "HTTP_ERROR", await response.text(), response.headers.get("x-request-id"));
    }
    return response.text();
  }
}

export function verifyWebhookSignature(options: {
  rawBody: string | Buffer;
  signatureHeader: string;
  secret: string;
  toleranceSeconds?: number;
  nowSeconds?: number;
}): boolean {
  const { rawBody, signatureHeader, secret, toleranceSeconds = 300, nowSeconds = Date.now() / 1000 } = options;
  const parts = Object.fromEntries(signatureHeader.split(",").map((part) => part.split("=", 2)));
  const timestamp = Number(parts.t);
  const supplied = parts.v1;
  if (!Number.isFinite(timestamp) || !supplied || Math.abs(nowSeconds - timestamp) >= toleranceSeconds) return false;
  const expected = createHmac("sha256", secret).update(`${parts.t}.`).update(rawBody).digest("hex");
  const givenBuffer = Buffer.from(supplied, "hex");
  const expectedBuffer = Buffer.from(expected, "hex");
  return givenBuffer.length === expectedBuffer.length && timingSafeEqual(givenBuffer, expectedBuffer);
}