import type { FetchOptions, PoliteFetcher } from "./types.js";
import { HttpStatusError, StorePausedError } from "./httpErrors.js";

export interface HttpTransportRequest {
  url: string;
  method: string;
  headers: Record<string, string>;
  body?: string;
  timeoutMs: number;
}

export interface HttpTransportResponse {
  status: number;
  headers: Record<string, string | readonly string[] | undefined>;
  body: string;
}

export type HttpTransport = (request: HttpTransportRequest) => Promise<HttpTransportResponse>;

export interface PoliteCoreOptions {
  minGapMs?: number;
  defaultTimeoutMs?: number;
  breakerThreshold?: number;
  breakerPauseMs?: number;
  transport: HttpTransport;
}

interface StoreState {
  lastRequestAt: number;
  consecutiveFailures: number;
  pausedUntil: number;
  cookies: Map<string, string>;
  lock: Promise<void>;
}

interface PortableRequestInit {
  method: string;
  headers?: Record<string, string>;
  body?: string;
}

const BROWSER_UA =
  "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36";

/** Runtime-neutral politeness, cookie, retry, and circuit-breaker implementation. */
export class PoliteCore implements PoliteFetcher {
  private states = new Map<string, StoreState>();
  private readonly minGapMs: number;
  private readonly defaultTimeoutMs: number;
  private readonly breakerThreshold: number;
  private readonly breakerPauseMs: number;
  private readonly transport: HttpTransport;

  constructor(opts: PoliteCoreOptions) {
    this.minGapMs = opts.minGapMs ?? 2000;
    this.defaultTimeoutMs = opts.defaultTimeoutMs ?? 8000;
    this.breakerThreshold = opts.breakerThreshold ?? 5;
    this.breakerPauseMs = opts.breakerPauseMs ?? 10 * 60 * 1000;
    this.transport = opts.transport;
  }

  private state(storeId: string): StoreState {
    let state = this.states.get(storeId);
    if (!state) {
      state = {
        lastRequestAt: 0,
        consecutiveFailures: 0,
        pausedUntil: 0,
        cookies: new Map(),
        lock: Promise.resolve(),
      };
      this.states.set(storeId, state);
    }
    return state;
  }

  async get(storeId: string, url: string, opts: FetchOptions = {}): Promise<string> {
    return this.request(storeId, url, { method: "GET" }, opts);
  }

  async postForm(
    storeId: string,
    url: string,
    form: Record<string, string>,
    opts: FetchOptions = {},
  ): Promise<string> {
    return this.request(
      storeId,
      url,
      {
        method: "POST",
        headers: { "Content-Type": "application/x-www-form-urlencoded" },
        body: new URLSearchParams(form).toString(),
      },
      opts,
    );
  }

  async postJson(
    storeId: string,
    url: string,
    payload: unknown,
    opts: FetchOptions = {},
  ): Promise<string> {
    return this.request(
      storeId,
      url,
      {
        method: "POST",
        headers: { "Content-Type": "application/json;charset=UTF-8" },
        body: JSON.stringify(payload),
      },
      opts,
    );
  }

  private async request(
    storeId: string,
    url: string,
    init: PortableRequestInit,
    opts: FetchOptions,
  ): Promise<string> {
    const state = this.state(storeId);
    const previous = state.lock;
    let release!: () => void;
    state.lock = new Promise((resolve) => (release = resolve));
    await previous;

    try {
      if (Date.now() < state.pausedUntil) throw new StorePausedError(storeId);

      const body = await this.attempt(storeId, url, init, opts).catch((firstError) => {
        if (firstError instanceof HttpStatusError && firstError.status < 500) throw firstError;
        return this.attempt(storeId, url, init, opts);
      });
      state.consecutiveFailures = 0;
      return body;
    } catch (error) {
      if (!(error instanceof StorePausedError) && countsTowardBreaker(error)) {
        state.consecutiveFailures++;
        if (state.consecutiveFailures >= this.breakerThreshold) {
          state.pausedUntil = Date.now() + this.breakerPauseMs;
          state.consecutiveFailures = 0;
        }
      }
      throw error;
    } finally {
      release();
    }
  }

  private async attempt(
    storeId: string,
    url: string,
    init: PortableRequestInit,
    opts: FetchOptions,
  ): Promise<string> {
    const state = this.state(storeId);
    const wait = state.lastRequestAt + this.minGapMs - Date.now();
    if (wait > 0) await new Promise((resolve) => setTimeout(resolve, wait));
    state.lastRequestAt = Date.now();

    const headers: Record<string, string> = {
      "User-Agent": BROWSER_UA,
      Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
      "Accept-Language": "lv-LV,lv;q=0.9,en;q=0.8",
      ...init.headers,
      ...opts.headers,
    };
    if (state.cookies.size > 0) {
      headers["Cookie"] = [...state.cookies.entries()].map(([key, value]) => `${key}=${value}`).join("; ");
    }

    const response = await this.transport({
      url,
      method: init.method,
      headers,
      ...(init.body === undefined ? {} : { body: init.body }),
      timeoutMs: opts.timeoutMs ?? this.defaultTimeoutMs,
    });

    for (const setCookie of headerValues(response.headers, "set-cookie")) {
      const pair = setCookie.split(";")[0];
      const equals = pair?.indexOf("=") ?? -1;
      if (pair && equals > 0) {
        state.cookies.set(pair.slice(0, equals).trim(), pair.slice(equals + 1).trim());
      }
    }

    if (response.status < 200 || response.status >= 300) {
      throw new HttpStatusError(storeId, url, response.status);
    }
    return response.body;
  }
}

function headerValues(
  headers: HttpTransportResponse["headers"],
  name: string,
): readonly string[] {
  const entry = Object.entries(headers).find(([key]) => key.toLowerCase() === name)?.[1];
  if (entry === undefined) return [];
  return typeof entry === "string" ? [entry] : entry;
}

function countsTowardBreaker(error: unknown): boolean {
  if (error instanceof HttpStatusError) {
    return error.status === 403 || error.status === 429 || error.status >= 500;
  }
  return true;
}
