import { z } from 'zod';
import type { HubSpotObjectType, HubSpotRecord, HubSpotRecordPage } from './contracts';

const HUBSPOT_API_BASE = 'https://api.hubapi.com';
const HUBSPOT_CRM_VERSION = '2026-03';
const HUBSPOT_LIST_LIMIT = 100;
const HUBSPOT_BATCH_LIMIT = 100;

export const HUBSPOT_READ_PROPERTIES: Readonly<Record<HubSpotObjectType, readonly string[]>> = {
  companies: [
    'name',
    'domain',
    'city',
    'state',
    'country',
    'industry',
    'numberofemployees',
    'annualrevenue',
    'hs_lastmodifieddate',
  ],
  contacts: [
    'email',
    'firstname',
    'lastname',
    'phone',
    'mobilephone',
    'jobtitle',
    'hs_lastmodifieddate',
  ],
  deals: [
    'dealname',
    'pipeline',
    'dealstage',
    'amount',
    'closedate',
    'hs_lastmodifieddate',
  ],
};

const recordSchema = z.object({
  id: z.string().min(1),
  properties: z.record(z.string().nullable()),
  createdAt: z.string().datetime(),
  updatedAt: z.string().datetime(),
  archived: z.boolean(),
}).passthrough();
const pagingAfterSchema = z.union([z.string(), z.number()]).transform(String);
const listResponseSchema = z.object({
  results: z.array(recordSchema),
  paging: z.object({ next: z.object({ after: pagingAfterSchema }).passthrough() }).passthrough().optional(),
}).passthrough();
const batchResponseSchema = z.object({ results: z.array(recordSchema) }).passthrough();

export class HubSpotApiError extends Error {
  constructor(
    message: string,
    readonly status?: number,
    readonly retryAfterSeconds?: number,
  ) {
    super(message);
    this.name = 'HubSpotApiError';
  }
}

export class HubSpotCrmClient {
  constructor(private readonly fetchImpl: typeof fetch = fetch) {}

  async listObjects(
    accessToken: string,
    objectType: HubSpotObjectType,
    options: { after?: string | null; signal?: AbortSignal } = {},
  ): Promise<HubSpotRecordPage> {
    const url = this.objectUrl(objectType);
    url.searchParams.set('limit', String(HUBSPOT_LIST_LIMIT));
    url.searchParams.set('archived', 'false');
    url.searchParams.set('properties', HUBSPOT_READ_PROPERTIES[objectType].join(','));
    if (options.after) url.searchParams.set('after', options.after);
    const response = await this.request(url, accessToken, { method: 'GET', signal: options.signal });
    const parsed = listResponseSchema.safeParse(await response.json());
    if (!parsed.success) throw new HubSpotApiError('HubSpot returned an invalid CRM list response.');
    return {
      results: parsed.data.results as HubSpotRecord[],
      nextAfter: parsed.data.paging?.next.after ?? null,
    };
  }

  async batchReadObjects(
    accessToken: string,
    objectType: HubSpotObjectType,
    ids: readonly string[],
    signal?: AbortSignal,
  ): Promise<HubSpotRecord[]> {
    if (ids.length === 0) return [];
    if (ids.length > HUBSPOT_BATCH_LIMIT) {
      throw new HubSpotApiError(`HubSpot batch reads accept at most ${HUBSPOT_BATCH_LIMIT} IDs.`);
    }
    if (ids.some((id) => id.length === 0)) throw new HubSpotApiError('HubSpot object IDs cannot be blank.');
    const url = new URL(`${this.objectUrl(objectType).toString()}/batch/read`);
    const response = await this.request(url, accessToken, {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({
        properties: HUBSPOT_READ_PROPERTIES[objectType],
        inputs: ids.map((id) => ({ id })),
      }),
      signal,
    });
    const parsed = batchResponseSchema.safeParse(await response.json());
    if (!parsed.success) throw new HubSpotApiError('HubSpot returned an invalid CRM batch response.');
    return parsed.data.results as HubSpotRecord[];
  }

  private objectUrl(objectType: HubSpotObjectType): URL {
    return new URL(`${HUBSPOT_API_BASE}/crm/objects/${HUBSPOT_CRM_VERSION}/${objectType}`);
  }

  private async request(
    url: URL,
    accessToken: string,
    init: RequestInit,
  ): Promise<Response> {
    const response = await this.fetchImpl(url, {
      ...init,
      headers: {
        ...init.headers,
        authorization: `Bearer ${accessToken}`,
        accept: 'application/json',
      },
    });
    if (!response.ok) {
      const retryAfter = response.headers.get('retry-after');
      const parsedRetryAfter = retryAfter === null ? undefined : Number(retryAfter);
      throw new HubSpotApiError(
        'HubSpot rejected the CRM request.',
        response.status,
        Number.isFinite(parsedRetryAfter) ? parsedRetryAfter : undefined,
      );
    }
    return response;
  }
}
