import type { FabroNativeClient, FabroNativeRun, FabroNativeRunStatus } from "./native.js";

const FACTORY_RUN_LABEL = "awp_factory_run_id";
const PLAN_REVISION_LABEL = "awp_plan_revision_id";
const IDEMPOTENCY_LABEL = "awp_idempotency_key";
const PAGE_LIMIT = 100;

interface FabroHttpClientOptions {
  readonly baseUrl: string;
  readonly bearerToken?: string;
  readonly fetch?: typeof globalThis.fetch;
  readonly workflowSource?: string;
}

interface FabroRunWire {
  readonly id: string;
  readonly labels: Readonly<Record<string, string>>;
  readonly lifecycle: {
    readonly status: {
      readonly kind: string;
      readonly reason?: unknown;
      readonly blocked_reason?: unknown;
    };
    readonly pending_control?: string | null;
  };
  readonly links?: { readonly web?: string | null };
}

interface FabroRunPageWire {
  readonly data: readonly FabroRunWire[];
  readonly meta: { readonly has_more: boolean };
}

interface FabroTimelineEntryWire {
  readonly ordinal: number;
  readonly node_name: string;
  readonly visit: number;
  readonly checkpoint_seq: number;
  readonly run_commit_sha?: string | null;
}

class FabroHttpError extends Error {
  constructor(
    readonly status: number,
    message: string,
  ) {
    super(message);
    this.name = "FabroHttpError";
  }
}

function escapeTomlBasicString(value: string): string {
  return JSON.stringify(value);
}

function defaultWorkflowSource(): string {
  return [
    "digraph AWPFactory {",
    '  graph [goal="AWP FactoryRun orchestration mechanics"]',
    "  start [shape=Mdiamond]",
    "  complete [shape=Msquare]",
    "  start -> complete",
    "}",
    "",
  ].join("\n");
}

function workflowConfig(input: {
  readonly factoryRunId: string;
  readonly planRevisionId: string;
  readonly idempotencyKey: string;
}): string {
  return [
    "_version = 1",
    "",
    "[run.metadata]",
    `${FACTORY_RUN_LABEL} = ${escapeTomlBasicString(input.factoryRunId)}`,
    `${PLAN_REVISION_LABEL} = ${escapeTomlBasicString(input.planRevisionId)}`,
    `${IDEMPOTENCY_LABEL} = ${escapeTomlBasicString(input.idempotencyKey)}`,
    "",
  ].join("\n");
}

function statusKind(run: FabroRunWire): FabroNativeRunStatus {
  const kind = run.lifecycle.status.kind;
  if (run.lifecycle.pending_control === "cancel") return "cancelling";
  switch (kind) {
    case "submitted":
    case "pending":
    case "runnable":
    case "starting":
      return "queued";
    case "running":
      return "running";
    case "blocked":
    case "paused":
      return "waiting";
    case "succeeded":
      return "completed";
    case "failed":
      return String(run.lifecycle.status.reason ?? "").toLowerCase() === "cancelled"
        ? "cancelled"
        : "failed";
    case "dead":
    case "removing":
      return "failed";
    default:
      throw new Error(`Unsupported Fabro run status: ${kind}`);
  }
}

function failureCode(run: FabroRunWire): string | undefined {
  if (statusKind(run) !== "failed") return undefined;
  const reason = run.lifecycle.status.reason;
  return typeof reason === "string" && reason.trim() ? reason : run.lifecycle.status.kind;
}

function requiredLabel(run: FabroRunWire, label: string): string {
  const value = run.labels[label]?.trim();
  if (!value) throw new Error(`Fabro run ${run.id} is missing required AWP label ${label}`);
  return value;
}

export class FabroHttpClient implements FabroNativeClient {
  private readonly baseUrl: string;
  private readonly bearerToken: string | undefined;
  private readonly fetchImpl: typeof globalThis.fetch;
  private readonly workflowSource: string;

  constructor(options: FabroHttpClientOptions) {
    const base = new URL(options.baseUrl);
    this.baseUrl = base.toString().replace(/\/$/u, "");
    this.bearerToken = options.bearerToken?.trim() || undefined;
    this.fetchImpl = options.fetch ?? globalThis.fetch;
    this.workflowSource = options.workflowSource ?? defaultWorkflowSource();
  }

  async findRunByIdempotencyKey(idempotencyKey: string): Promise<FabroNativeRun | undefined> {
    return this.findRunByLabel(IDEMPOTENCY_LABEL, idempotencyKey);
  }

  async findRunByFactoryRunId(factoryRunId: string): Promise<FabroNativeRun | undefined> {
    return this.findRunByLabel(FACTORY_RUN_LABEL, factoryRunId);
  }

  async startRun(input: {
    readonly factoryRunId: string;
    readonly planRevisionId: string;
    readonly idempotencyKey: string;
  }): Promise<FabroNativeRun> {
    let created: FabroRunWire;
    try {
      created = await this.request<FabroRunWire>("/api/v1/runs", {
        method: "POST",
        body: JSON.stringify({
          version: 1,
          title: `AWP FactoryRun ${input.factoryRunId}`,
          cwd: "/workspace",
          target: { path: "awp-factory.fabro" },
          workflows: {
            "awp-factory.fabro": {
              source: this.workflowSource,
              config: {
                path: "workflow.toml",
                source: workflowConfig(input),
              },
            },
          },
        }),
      });
    } catch (error) {
      const reconciled = await this.findRunByIdempotencyKey(input.idempotencyKey).catch(
        () => undefined,
      );
      if (reconciled !== undefined) return reconciled;
      throw error;
    }

    try {
      const started = await this.request<FabroRunWire>(
        `/api/v1/runs/${encodeURIComponent(created.id)}/start`,
        { method: "POST", body: "{}" },
      );
      return this.toNative(started);
    } catch (error) {
      const observed = await this.getRun(created.id).catch(() => undefined);
      if (observed !== undefined && observed.status !== "queued") return observed;
      throw error;
    }
  }

  async cancelRun(nativeRunId: string): Promise<FabroNativeRun> {
    const run = await this.request<FabroRunWire>(
      `/api/v1/runs/${encodeURIComponent(nativeRunId)}/cancel`,
      { method: "POST" },
    );
    return this.toNative(run);
  }

  async getRun(nativeRunId: string): Promise<FabroNativeRun | undefined> {
    const response = await this.rawRequest(`/api/v1/runs/${encodeURIComponent(nativeRunId)}`);
    if (response.status === 404) return undefined;
    const run = await this.decode<FabroRunWire>(response);
    return this.toNative(run);
  }

  private async findRunByLabel(
    label: string,
    expected: string,
  ): Promise<FabroNativeRun | undefined> {
    let offset = 0;
    while (true) {
      const query = new URLSearchParams({
        "page[limit]": String(PAGE_LIMIT),
        "page[offset]": String(offset),
        include_archived: "true",
        sort: "created_at",
        direction: "desc",
      });
      const page = await this.request<FabroRunPageWire>(`/api/v1/runs?${query}`);
      const match = page.data.find((run) => run.labels[label] === expected);
      if (match) return this.toNative(match);
      if (!page.meta.has_more) return undefined;
      offset += PAGE_LIMIT;
    }
  }

  private async toNative(run: FabroRunWire): Promise<FabroNativeRun> {
    const timeline = await this.request<readonly FabroTimelineEntryWire[]>(
      `/api/v1/runs/${encodeURIComponent(run.id)}/timeline`,
    ).catch(() => []);
    const checkpoint = [...timeline].sort((left, right) => right.ordinal - left.ordinal)[0];
    const failure = failureCode(run);
    return {
      id: run.id,
      factoryRunId: requiredLabel(run, FACTORY_RUN_LABEL),
      planRevisionId: requiredLabel(run, PLAN_REVISION_LABEL),
      status: statusKind(run),
      ...(checkpoint === undefined ? {} : { currentStage: checkpoint.node_name }),
      ...(checkpoint === undefined ? {} : { checkpointId: String(checkpoint.checkpoint_seq) }),
      ...(failure === undefined ? {} : { failureCode: failure }),
      ...(run.links?.web ? { url: run.links.web } : {}),
    };
  }

  private async request<T>(path: string, init: RequestInit = {}): Promise<T> {
    return this.decode<T>(await this.rawRequest(path, init));
  }

  private async rawRequest(path: string, init: RequestInit = {}): Promise<Response> {
    const headers = new Headers(init.headers);
    headers.set("accept", "application/json");
    if (init.body !== undefined) headers.set("content-type", "application/json");
    if (this.bearerToken) headers.set("authorization", `Bearer ${this.bearerToken}`);
    return this.fetchImpl(`${this.baseUrl}${path}`, { ...init, headers });
  }

  private async decode<T>(response: Response): Promise<T> {
    if (!response.ok) {
      const body = await response.text().catch(() => "");
      throw new FabroHttpError(response.status, body.trim() || `Fabro returned ${response.status}`);
    }
    return (await response.json()) as T;
  }
}
