/**
 * `http://` and `https://` adapter.
 *
 * Remote content is data, never instruction. Whatever comes back is marked
 * untrusted unless the object declares a digest that matches, in which case it
 * is `verified` — integrity is a claim about bytes, not about intent, so even a
 * digest-matched document never becomes `trusted`.
 */

import type { Adapter, AdapterContext, AdapterResult } from "../types.js";
import { sha256Uri } from "../digest.js";

export class OfflineError extends Error {
  constructor(uri: string) {
    super(`Cannot fetch ${uri} in --offline mode. Run without --offline, or inline the content.`);
    this.name = "OfflineError";
  }
}

const DEFAULT_TIMEOUT_MS = 10_000;
const MAX_BYTES = 5 * 1024 * 1024;

async function readBody(response: Response, uri: string): Promise<string> {
  if (!response.body) return "";

  const reader = response.body.getReader();
  const chunks: Uint8Array[] = [];
  let totalBytes = 0;

  try {
    while (true) {
      const { done, value } = await reader.read();
      if (done) break;

      totalBytes += value.byteLength;
      if (totalBytes > MAX_BYTES) {
        await reader.cancel().catch(() => undefined);
        throw new Error(`Refusing to load ${uri}: response exceeds the ${MAX_BYTES} byte limit.`);
      }
      chunks.push(value);
    }
  } finally {
    reader.releaseLock();
  }

  const bytes = new Uint8Array(totalBytes);
  let offset = 0;
  for (const chunk of chunks) {
    bytes.set(chunk, offset);
    offset += chunk.byteLength;
  }
  return new TextDecoder().decode(bytes);
}

export const httpAdapter: Adapter = {
  name: "http",
  schemes: ["http", "https"],
  remote: true,
  async load(uri: string, ctx: AdapterContext): Promise<AdapterResult> {
    if (ctx.offline) throw new OfflineError(uri);

    const url = new URL(uri);
    if (url.protocol === "http:" && ctx.config.allow_insecure !== true) {
      throw new Error(
        `Refusing to fetch ${uri} over plaintext http. Use https, or set adapters.http.allow_insecure: true ` +
          `if this is a trusted network you control.`
      );
    }

    const timeoutMs = ctx.timeoutMs ?? (ctx.config.timeout_ms as number | undefined) ?? DEFAULT_TIMEOUT_MS;
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), timeoutMs);

    try {
      let response: Response;
      try {
        response = await fetch(url, {
          signal: controller.signal,
          // Redirects can move a request to a host the author never named, so the
          // final URL is reported back rather than followed silently.
          redirect: "follow",
          headers: { accept: "text/markdown, text/plain, application/json;q=0.9, */*;q=0.8" }
        });
      } catch (error) {
        throw new Error(`Failed to fetch ${uri}: ${(error as Error).message}`);
      }

      if (!response.ok) {
        throw new Error(`Failed to fetch ${uri}: HTTP ${response.status} ${response.statusText}`);
      }

      const declaredLength = Number(response.headers.get("content-length") ?? "0");
      if (declaredLength > MAX_BYTES) {
        throw new Error(`Refusing to load ${uri}: ${declaredLength} bytes exceeds the ${MAX_BYTES} byte limit.`);
      }

      const content = await readBody(response, uri);

      return {
        content,
        contentType: (response.headers.get("content-type") ?? "text/plain").split(";")[0]!.trim(),
        digest: sha256Uri(content),
        retrievedAt: new Date().toISOString(),
        trust: (ctx.config.trust as AdapterResult["trust"]) ?? "untrusted"
      };
    } finally {
      clearTimeout(timer);
    }
  }
};
