import type {
  DescribedProvider,
  DurableWorkflowProvider,
  ProviderObservation,
  ReconcileRequest,
} from "@awp/application";
import {
  unsafeOpaqueId,
  type OperationId,
  type ProviderDescriptor,
  type ProviderId,
  type ProviderOperationContext,
  type ProviderReference,
  type ProviderResult,
} from "@awp/contracts";
import type { DbosRuntime, DbosWorkflowRecord } from "./dbos-bridge.js";

const PROVIDER_ID = unsafeOpaqueId<ProviderId>("dbos");

export class DbosWorkflowProvider implements DurableWorkflowProvider, DescribedProvider {
  constructor(
    private readonly runtime: DbosRuntime,
    private readonly now: () => string = () => new Date().toISOString(),
  ) {}

  async describe(): Promise<ProviderDescriptor> {
    return {
      providerId: PROVIDER_ID,
      kind: "durable-workflow",
      adapterVersion: "i0-v1",
      capabilities: [
        "workflow.start",
        "workflow.cancel",
        "workflow.reconcile",
        "workflow.duplicate-return-existing",
        "workflow.restart-recovery",
      ],
      authModes: ["control-plane-runtime"],
      resourceTypes: ["dbos-workflow"],
      healthFeatures: ["status-readback", "durable-postgres-state"],
    };
  }

  async start(
    context: ProviderOperationContext,
    operationId: OperationId,
    workflowKind: string,
    input: unknown,
  ): Promise<ProviderResult<ProviderObservation>> {
    const workflowId = workflowIdFor(operationId);
    const existing = await this.runtime.get(workflowId);
    if (existing) return this.result(existing);

    try {
      const started = await this.runtime.start({
        workflowId,
        workflowKind,
        input,
        idempotencyKey: context.idempotencyKey,
        correlationId: context.correlationId,
        operationId,
      });
      return this.result(started);
    } catch (error) {
      // A timeout after DBOS durably accepted the workflow is ambiguous. Reconcile
      // by the deterministic workflow ID before allowing a higher layer to retry.
      const reconciled = await this.runtime.get(workflowId);
      if (reconciled) return this.result(reconciled);
      throw error;
    }
  }

  async cancel(
    _context: ProviderOperationContext,
    operationId: OperationId,
  ): Promise<ProviderResult<ProviderObservation>> {
    const workflowId = workflowIdFor(operationId);
    const existing = await this.runtime.get(workflowId);
    if (!existing) {
      return this.absentResult(workflowId, "cancelled-absent");
    }

    await this.runtime.cancel(workflowId);
    const cancelled = await this.runtime.get(workflowId);
    if (!cancelled) return this.absentResult(workflowId, "cancelled-absent");
    return this.result(cancelled);
  }

  async reconcile(request: ReconcileRequest): Promise<ProviderResult<ProviderObservation>> {
    validateReference(request.reference);
    const workflow = await this.runtime.get(request.reference.nativeId);
    if (!workflow) {
      return this.absentResult(request.reference.nativeId, "missing");
    }
    return this.result(workflow);
  }

  private result(workflow: DbosWorkflowRecord): ProviderResult<ProviderObservation> {
    const observedAt = workflow.updatedAt ?? this.now();
    const reference = referenceFor(workflow, observedAt);
    return {
      value: {
        state: normalizeStatus(workflow.status),
        details: {
          workflowKind: workflow.workflowKind,
          dbosStatus: workflow.status,
          workflowId: workflow.workflowId,
          durable: true,
          ...(workflow.output === undefined ? {} : { output: workflow.output }),
          ...(workflow.error === undefined ? {} : { error: safeError(workflow.error) }),
        },
      },
      references: [reference],
      observedAt,
      reconciliationToken: `${workflow.workflowId}:${workflow.status}`,
    };
  }

  private absentResult(workflowId: string, state: string): ProviderResult<ProviderObservation> {
    const observedAt = this.now();
    return {
      value: {
        state,
        details: { workflowId, durable: true },
      },
      references: [
        {
          providerId: PROVIDER_ID,
          resourceType: "dbos-workflow",
          nativeId: workflowId,
          observedAt,
        },
      ],
      observedAt,
      reconciliationToken: `${workflowId}:${state}`,
    };
  }
}

export function workflowIdFor(operationId: OperationId): string {
  return `awp:${operationId}`;
}

function referenceFor(workflow: DbosWorkflowRecord, observedAt: string): ProviderReference {
  return {
    providerId: PROVIDER_ID,
    resourceType: "dbos-workflow",
    nativeId: workflow.workflowId,
    ...(workflow.updatedAt === undefined ? {} : { nativeRevision: workflow.updatedAt }),
    observedAt,
  };
}

function validateReference(reference: ProviderReference): void {
  if (reference.providerId !== PROVIDER_ID || reference.resourceType !== "dbos-workflow") {
    throw new Error("DBOS reconcile received a foreign provider reference");
  }
}

function normalizeStatus(status: DbosWorkflowRecord["status"]): string {
  switch (status) {
    case "ENQUEUED":
    case "DELAYED":
      return "waiting";
    case "PENDING":
      return "running";
    case "SUCCESS":
      return "succeeded";
    case "CANCELLED":
      return "cancelled";
    case "ERROR":
    case "MAX_RECOVERY_ATTEMPTS_EXCEEDED":
      return "failed";
  }
}

function safeError(error: unknown): string {
  if (error instanceof Error) return error.message;
  return typeof error === "string" ? error : "DBOS workflow failed";
}
