import { readFileSync } from "node:fs";
import { request as httpsRequest } from "node:https";
import type { KubernetesObject, KubernetesTransport } from "./kubernetes.js";

interface KubernetesFetchResponse {
  readonly ok: boolean;
  readonly status: number;
  readonly statusText: string;
  json(): Promise<unknown>;
  text(): Promise<string>;
}

export type KubernetesFetch = (
  url: string,
  init: {
    method: string;
    headers: Readonly<Record<string, string>>;
    body?: string;
  },
) => Promise<KubernetesFetchResponse>;

export interface KubernetesApiTransportOptions {
  readonly apiBase: string;
  readonly bearerToken: string;
  readonly fieldManager?: string;
  readonly fetcher?: KubernetesFetch;
}

/**
 * Minimal Kubernetes HTTP transport for the exact resource families owned by
 * WorkspaceProvider. It uses server-side apply and never exposes Kubernetes SDK
 * objects outside this provider package.
 */
export class KubernetesApiTransport implements KubernetesTransport {
  private readonly apiBase: string;
  private readonly bearerToken: string;
  private readonly fieldManager: string;
  private readonly fetcher: KubernetesFetch;

  constructor(options: KubernetesApiTransportOptions) {
    this.apiBase = options.apiBase.replace(/\/$/, "");
    this.bearerToken = options.bearerToken;
    this.fieldManager = options.fieldManager ?? "awp-workspace-provider";
    this.fetcher = options.fetcher ?? (globalThis.fetch as unknown as KubernetesFetch);
  }

  static fromInCluster(
    env: Readonly<Record<string, string | undefined>> = process.env,
    tokenPath = "/var/run/secrets/kubernetes.io/serviceaccount/token",
    caPath = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt",
  ): KubernetesApiTransport {
    const host = env.KUBERNETES_SERVICE_HOST;
    const port = env.KUBERNETES_SERVICE_PORT_HTTPS ?? env.KUBERNETES_SERVICE_PORT ?? "443";
    if (!host) throw new Error("KUBERNETES_SERVICE_HOST is required for in-cluster transport");
    const token = readFileSync(tokenPath, "utf8").trim();
    if (!token) throw new Error("Kubernetes service-account token is empty");
    const ca = readFileSync(caPath, "utf8");
    if (!ca.trim()) throw new Error("Kubernetes service-account CA certificate is empty");
    return new KubernetesApiTransport({
      apiBase: `https://${host}:${port}`,
      bearerToken: token,
      fetcher: createNodeHttpsFetch(ca),
    });
  }

  async apply(resource: KubernetesObject): Promise<KubernetesObject> {
    const url = `${resourceUrl(this.apiBase, resource.apiVersion, resource.kind, resource.metadata.namespace, resource.metadata.name)}?fieldManager=${encodeURIComponent(this.fieldManager)}&force=true`;
    const response = await this.fetcher(url, {
      method: "PATCH",
      headers: this.headers("application/apply-patch+yaml"),
      body: JSON.stringify(resource),
    });
    return this.requireObject(response, `apply ${resource.kind}/${resource.metadata.name}`);
  }

  async get(
    apiVersion: string,
    kind: string,
    namespace: string | undefined,
    name: string,
  ): Promise<KubernetesObject | undefined> {
    const response = await this.fetcher(
      resourceUrl(this.apiBase, apiVersion, kind, namespace, name),
      {
        method: "GET",
        headers: this.headers("application/json"),
      },
    );
    if (response.status === 404) return undefined;
    return this.requireObject(response, `get ${kind}/${name}`);
  }

  async annotate(
    apiVersion: string,
    kind: string,
    namespace: string | undefined,
    name: string,
    annotations: Readonly<Record<string, string>>,
  ): Promise<KubernetesObject> {
    const response = await this.fetcher(
      resourceUrl(this.apiBase, apiVersion, kind, namespace, name),
      {
        method: "PATCH",
        headers: this.headers("application/merge-patch+json"),
        body: JSON.stringify({ metadata: { annotations } }),
      },
    );
    return this.requireObject(response, `annotate ${kind}/${name}`);
  }

  async delete(
    apiVersion: string,
    kind: string,
    namespace: string | undefined,
    name: string,
  ): Promise<void> {
    const response = await this.fetcher(
      resourceUrl(this.apiBase, apiVersion, kind, namespace, name),
      {
        method: "DELETE",
        headers: this.headers("application/json"),
        body: JSON.stringify({
          apiVersion: "v1",
          kind: "DeleteOptions",
          propagationPolicy: "Background",
        }),
      },
    );
    if (response.status === 404) return;
    if (!response.ok) throw await kubernetesHttpError(response, `delete ${kind}/${name}`);
  }

  private headers(contentType: string): Readonly<Record<string, string>> {
    return {
      Accept: "application/json",
      Authorization: `Bearer ${this.bearerToken}`,
      "Content-Type": contentType,
    };
  }

  private async requireObject(
    response: KubernetesFetchResponse,
    operation: string,
  ): Promise<KubernetesObject> {
    if (!response.ok) throw await kubernetesHttpError(response, operation);
    const value = await response.json();
    if (!isKubernetesObject(value)) {
      throw new Error(`Kubernetes API returned an invalid object for ${operation}`);
    }
    return value;
  }
}

export function createNodeHttpsFetch(ca: string): KubernetesFetch {
  return async (url, init) =>
    new Promise<KubernetesFetchResponse>((resolve, reject) => {
      const request = httpsRequest(
        new URL(url),
        {
          method: init.method,
          headers: { ...init.headers },
          ca,
        },
        (response) => {
          const chunks: Buffer[] = [];
          response.on("data", (chunk: Buffer | string) => {
            chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
          });
          response.on("error", reject);
          response.on("end", () => {
            const body = Buffer.concat(chunks).toString("utf8");
            const status = response.statusCode ?? 0;
            resolve({
              ok: status >= 200 && status < 300,
              status,
              statusText: response.statusMessage ?? "",
              async text() {
                return body;
              },
              async json() {
                return body ? JSON.parse(body) : {};
              },
            });
          });
        },
      );
      request.on("error", reject);
      if (init.body !== undefined) request.write(init.body);
      request.end();
    });
}

function resourceUrl(
  apiBase: string,
  apiVersion: string,
  kind: string,
  namespace: string | undefined,
  name: string,
): string {
  const groupPrefix = apiVersion === "v1" ? "/api/v1" : `/apis/${apiVersion}`;
  const plural = resourcePlural(apiVersion, kind);
  const namespaceSegment = namespace ? `/namespaces/${encodeURIComponent(namespace)}` : "";
  return `${apiBase}${groupPrefix}${namespaceSegment}/${plural}/${encodeURIComponent(name)}`;
}

function resourcePlural(apiVersion: string, kind: string): string {
  const key = `${apiVersion}:${kind}`;
  switch (key) {
    case "v1:Pod":
      return "pods";
    case "v1:PersistentVolumeClaim":
      return "persistentvolumeclaims";
    case "v1:ServiceAccount":
      return "serviceaccounts";
    case "v1:Service":
      return "services";
    case "v1:Secret":
      return "secrets";
    case "networking.k8s.io/v1:NetworkPolicy":
      return "networkpolicies";
    default:
      throw new Error(`Unsupported Kubernetes resource for WorkspaceProvider: ${key}`);
  }
}

function isKubernetesObject(value: unknown): value is KubernetesObject {
  if (!value || typeof value !== "object" || Array.isArray(value)) return false;
  const object = value as Readonly<Record<string, unknown>>;
  if (typeof object.apiVersion !== "string" || typeof object.kind !== "string") return false;
  if (!object.metadata || typeof object.metadata !== "object" || Array.isArray(object.metadata))
    return false;
  const metadata = object.metadata as Readonly<Record<string, unknown>>;
  return typeof metadata.name === "string";
}

async function kubernetesHttpError(
  response: KubernetesFetchResponse,
  operation: string,
): Promise<Error> {
  let reason = "";
  try {
    const body = await response.text();
    const parsed: unknown = JSON.parse(body);
    if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
      const object = parsed as Readonly<Record<string, unknown>>;
      if (typeof object.reason === "string") reason = object.reason;
      else if (typeof object.message === "string") reason = object.message;
    }
  } catch {
    // Keep the error safe and bounded if the API returns non-JSON content.
  }
  return new Error(
    `Kubernetes API ${operation} failed: ${response.status} ${reason || response.statusText}`.trim(),
  );
}
