import { PicloudHttpError, type AuthTokenProvider, type Validator } from './types.js';

type Method = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';

export interface EndpointConfig {
  baseURL: string;
  path: string;
  getAuthToken?: AuthTokenProvider;
  fetchImpl: typeof fetch;
}

export interface RequestOptions<Res> {
  /** Extra headers merged over the defaults. */
  headers?: Record<string, string>;
  /** Optional runtime validation of the parsed response. */
  validate?: Validator<Res>;
  /** AbortSignal to cancel the request. */
  signal?: AbortSignal;
}

/**
 * Typed HTTP to a dev-defined script endpoint. Auth header injection +
 * structured errors; the request/response types are caller-supplied
 * generics (`endpoint<Req, Res>('/path')`). No service access — every
 * call hits a route a script binds (the hybrid model).
 */
export class Endpoint<Req = unknown, Res = unknown> {
  constructor(private readonly cfg: EndpointConfig) {}

  get(opts?: RequestOptions<Res>): Promise<Res> {
    return this.send('GET', undefined, opts);
  }

  post(body?: Req, opts?: RequestOptions<Res>): Promise<Res> {
    return this.send('POST', body, opts);
  }

  put(body?: Req, opts?: RequestOptions<Res>): Promise<Res> {
    return this.send('PUT', body, opts);
  }

  patch(body?: Req, opts?: RequestOptions<Res>): Promise<Res> {
    return this.send('PATCH', body, opts);
  }

  delete(opts?: RequestOptions<Res>): Promise<Res> {
    return this.send('DELETE', undefined, opts);
  }

  private async send(method: Method, body: Req | undefined, opts?: RequestOptions<Res>): Promise<Res> {
    const headers: Record<string, string> = {
      Accept: 'application/json',
      ...(opts?.headers ?? {})
    };
    if (body !== undefined) {
      headers['Content-Type'] ??= 'application/json';
    }
    const token = this.cfg.getAuthToken ? await this.cfg.getAuthToken() : undefined;
    if (token) {
      headers['Authorization'] ??= `Bearer ${token}`;
    }

    const url = joinUrl(this.cfg.baseURL, this.cfg.path);
    const init: RequestInit = { method, headers };
    if (body !== undefined) {
      init.body = JSON.stringify(body);
    }
    if (opts?.signal) {
      init.signal = opts.signal;
    }

    const res = await this.cfg.fetchImpl(url, init);
    const parsed = await parseBody(res);
    if (!res.ok) {
      const message =
        (isRecord(parsed) && typeof parsed['error'] === 'string' && parsed['error']) ||
        `${method} ${this.cfg.path} failed with ${res.status}`;
      throw new PicloudHttpError(res.status, message, parsed);
    }
    return opts?.validate ? opts.validate.parse(parsed) : (parsed as Res);
  }
}

async function parseBody(res: Response): Promise<unknown> {
  const text = await res.text();
  if (text.length === 0) return null;
  const ct = res.headers.get('content-type') ?? '';
  if (ct.includes('application/json')) {
    try {
      return JSON.parse(text);
    } catch {
      return text;
    }
  }
  return text;
}

function isRecord(v: unknown): v is Record<string, unknown> {
  return typeof v === 'object' && v !== null;
}

export function joinUrl(base: string, path: string): string {
  const b = base.endsWith('/') ? base.slice(0, -1) : base;
  const p = path.startsWith('/') ? path : `/${path}`;
  return `${b}${p}`;
}
