import { issueModelGatewayCapability } from "@awp/contracts";
import type {
  PlanningModelCatalogEntry,
  PlanningModelInvocationRequest,
  PlanningModelInvocationResult,
  PlanningModelProvider,
} from "@awp/application";

export class PlanningModelGatewayError extends Error {
  constructor(
    readonly category: string,
    readonly retryable: boolean,
    readonly safeMessage: string,
  ) {
    super(safeMessage);
    this.name = "PlanningModelGatewayError";
  }
}

function ssePayload(text: string): {
  readonly outputText: string;
  readonly invocationId?: string;
  readonly observedReasoningEffort?: string;
} {
  const deltas: string[] = [];
  let completedText: string | undefined;
  let invocationId: string | undefined;
  let observedReasoningEffort: string | undefined;
  for (const block of text.split(/\r?\n\r?\n/u)) {
    const dataLines = block
      .split(/\r?\n/u)
      .filter((line) => line.startsWith("data:"))
      .map((line) => line.slice(5).trim());
    if (dataLines.length === 0) continue;
    const data = dataLines.join("\n");
    if (!data || data === "[DONE]") continue;
    let parsed: unknown;
    try {
      parsed = JSON.parse(data);
    } catch {
      throw new PlanningModelGatewayError(
        "adapter-protocol",
        true,
        "Planner model gateway returned malformed event data",
      );
    }
    if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) continue;
    const event = parsed as Record<string, unknown>;
    const response = event.response;
    if (response && typeof response === "object" && !Array.isArray(response)) {
      const responseRecord = response as Record<string, unknown>;
      const id = responseRecord.id;
      if (typeof id === "string" && id.trim()) invocationId = id.trim();
      const reasoning = responseRecord.reasoning;
      if (reasoning && typeof reasoning === "object" && !Array.isArray(reasoning)) {
        const effort = (reasoning as Record<string, unknown>).effort;
        if (typeof effort === "string" && effort.trim()) observedReasoningEffort = effort.trim();
      }
    }
    if (event.type === "response.output_text.delta" && typeof event.delta === "string") {
      deltas.push(event.delta);
    }
    if (event.type === "response.output_text.done" && typeof event.text === "string") {
      completedText = event.text;
    }
    if (event.type === "response.failed" || event.type === "error") {
      throw new PlanningModelGatewayError(
        "provider-failure",
        true,
        "Planner model failed while streaming a response",
      );
    }
  }
  const outputText = (completedText ?? deltas.join("")).trim();
  if (!outputText) {
    throw new PlanningModelGatewayError(
      "adapter-protocol",
      true,
      "Planner model returned no output text",
    );
  }
  return {
    outputText,
    ...(invocationId ? { invocationId } : {}),
    ...(observedReasoningEffort ? { observedReasoningEffort } : {}),
  };
}

function failureFor(status: number): PlanningModelGatewayError {
  if (status === 401 || status === 403) {
    return new PlanningModelGatewayError(
      "auth",
      false,
      "Planner model authorization failed; verify the selected account and connection",
    );
  }
  if (status === 404) {
    return new PlanningModelGatewayError(
      "missing-model",
      false,
      "Planner model endpoint or selected model is unavailable",
    );
  }
  if (status === 429) {
    return new PlanningModelGatewayError(
      "rate-capacity",
      true,
      "Planner model is temporarily rate-limited or out of capacity",
    );
  }
  return new PlanningModelGatewayError(
    status >= 500 ? "unavailable" : "provider-failure",
    status >= 500,
    `Planner model invocation failed (${status})`,
  );
}

function modelCatalog(payload: unknown): readonly PlanningModelCatalogEntry[] {
  if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
    throw new PlanningModelGatewayError(
      "adapter-protocol",
      true,
      "Planner model catalog is invalid",
    );
  }
  const models = (payload as Record<string, unknown>).models;
  if (!Array.isArray(models)) {
    throw new PlanningModelGatewayError(
      "adapter-protocol",
      true,
      "Planner model catalog is missing models",
    );
  }
  return models.flatMap((value) => {
    if (!value || typeof value !== "object" || Array.isArray(value)) return [];
    const record = value as Record<string, unknown>;
    if (
      typeof record.slug !== "string" ||
      !record.slug.trim() ||
      record.supported_in_api === false ||
      record.visibility === "hide"
    ) {
      return [];
    }
    const efforts = Array.isArray(record.supported_reasoning_levels)
      ? record.supported_reasoning_levels.flatMap((item) => {
          if (!item || typeof item !== "object" || Array.isArray(item)) return [];
          const effort = (item as Record<string, unknown>).effort;
          const description = (item as Record<string, unknown>).description;
          if (typeof effort !== "string" || !effort.trim()) return [];
          return [
            {
              effort: effort.trim(),
              ...(typeof description === "string" && description.trim()
                ? { description: description.trim() }
                : {}),
            },
          ];
        })
      : [];
    return [
      {
        model: record.slug.trim(),
        displayName:
          typeof record.display_name === "string" && record.display_name.trim()
            ? record.display_name.trim()
            : record.slug.trim(),
        ...(typeof record.description === "string" && record.description.trim()
          ? { description: record.description.trim() }
          : {}),
        ...(typeof record.default_reasoning_level === "string" &&
        record.default_reasoning_level.trim()
          ? { defaultReasoningEffort: record.default_reasoning_level.trim() }
          : {}),
        reasoningEfforts: efforts,
        ...(typeof record.context_window === "number" && Number.isFinite(record.context_window)
          ? { contextWindow: record.context_window }
          : {}),
      },
    ];
  });
}

export class ModelGatewayPlanningProvider implements PlanningModelProvider {
  private readonly baseUrl: string;

  constructor(
    baseUrl: string,
    private readonly signingSecret: string,
    private readonly fetchImpl: typeof globalThis.fetch = globalThis.fetch,
  ) {
    const parsed = new URL(baseUrl);
    if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
      throw new Error("Planner Model Gateway URL must use http or https");
    }
    if (!signingSecret.trim()) throw new Error("Planner Model Gateway signing secret is empty");
    this.baseUrl = parsed.toString().replace(/\/$/u, "");
  }

  supportsProvider(providerId: string): boolean {
    return providerId.trim().toLowerCase() === "codex";
  }

  async listModels(accountId: string): Promise<readonly PlanningModelCatalogEntry[]> {
    const normalizedAccountId = accountId.trim();
    if (!normalizedAccountId) throw new Error("Planner model catalog requires an account");
    const capability = issueModelGatewayCapability(this.signingSecret, {
      purpose: "planning",
      planningSessionId: `planning:catalog:${normalizedAccountId}`,
      planningTurnId: "planning-turn:catalog",
      accountId: normalizedAccountId,
      ttlSeconds: 15 * 60,
    });
    let response: Response;
    try {
      response = await this.fetchImpl(`${this.baseUrl}/models?client_version=0.149.1`, {
        headers: { authorization: `Bearer ${capability}`, accept: "application/json" },
        signal: AbortSignal.timeout(30_000),
      });
    } catch {
      throw new PlanningModelGatewayError(
        "unavailable",
        true,
        "Planner model catalog is unavailable",
      );
    }
    if (!response.ok) throw failureFor(response.status);
    let payload: unknown;
    try {
      payload = await response.json();
    } catch {
      throw new PlanningModelGatewayError(
        "adapter-protocol",
        true,
        "Planner model catalog returned invalid JSON",
      );
    }
    return modelCatalog(payload);
  }

  async invoke(request: PlanningModelInvocationRequest): Promise<PlanningModelInvocationResult> {
    const capability = issueModelGatewayCapability(this.signingSecret, {
      purpose: "planning",
      planningSessionId: String(request.sessionId),
      planningTurnId: String(request.turnId),
      accountId: request.accountId,
      ttlSeconds: 15 * 60,
    });
    let response: Response;
    try {
      response = await this.fetchImpl(`${this.baseUrl}/responses`, {
        method: "POST",
        headers: {
          authorization: `Bearer ${capability}`,
          "content-type": "application/json",
          accept: "text/event-stream",
        },
        body: JSON.stringify({
          model: request.model,
          reasoning: { effort: request.reasoningEffort },
          input: [
            {
              role: "user",
              content: [{ type: "input_text", text: request.prompt }],
            },
          ],
          store: false,
          stream: true,
        }),
        signal: AbortSignal.timeout(120_000),
      });
    } catch {
      throw new PlanningModelGatewayError(
        "unavailable",
        true,
        "Planner model gateway is unavailable",
      );
    }
    if (!response.ok) throw failureFor(response.status);
    let body: string;
    try {
      body = await response.text();
    } catch {
      throw new PlanningModelGatewayError(
        "adapter-protocol",
        true,
        "Planner model gateway response stream could not be read",
      );
    }
    const parsed = ssePayload(body);
    if (
      parsed.observedReasoningEffort &&
      parsed.observedReasoningEffort !== request.reasoningEffort
    ) {
      throw new PlanningModelGatewayError(
        "adapter-protocol",
        false,
        `Planner provider applied reasoning effort ${parsed.observedReasoningEffort} instead of ${request.reasoningEffort}`,
      );
    }
    return {
      providerId: "provider:model-gateway",
      outputText: parsed.outputText,
      ...(parsed.invocationId ? { invocationId: parsed.invocationId } : {}),
      ...(parsed.observedReasoningEffort
        ? { observedReasoningEffort: parsed.observedReasoningEffort }
        : {}),
    };
  }
}
