/**
 * ADR-0027: Organization-scoped capability connection service. Manages the
 * lifecycle (rotate / read / disable) of capability credentials stored in
 * ADR-0024 encrypted envelopes with purpose="capability".
 *
 * Mirrors FeishuApplicationConnectionService, but keyed by (organizationId,
 * capabilityId) instead of 1:1 — an org may have multiple capabilities.
 */
import { randomUUID } from "node:crypto";
import type { Prisma, PrismaClient } from "@prisma/client";
import { lockActiveOrganization } from "../org/status.js";
import { LocalSecretEnvelope } from "../security/secretEnvelope.js";
import { probeDocmindCredential, type CapabilityReadinessProbe } from "./capabilityReadiness.js";
import type { CapabilitySecretPayload } from "./types.js";

const CAPABILITY_ID_PATTERN = /^[a-z0-9][a-z0-9._-]{0,63}$/;
const KNOWN_CAPABILITY_IDS = new Set(["pdf_to_md_bundle", "audio_video_to_text"]);

export interface CapabilityCredentialInput {
  readonly accessKeyId: string;
  readonly accessKeySecret: string;
  readonly endpoint: string;
}

export interface RotateCapabilityInput extends CapabilityCredentialInput {
  readonly organizationId: string;
  readonly capabilityId: string;
  readonly actorUserId: string;
}

export interface CapabilityConnectionMetadata {
  readonly id: string;
  readonly capabilityId: string;
  readonly status: "DRAFT" | "ACTIVE" | "DISABLED";
  readonly activeVersion: number | null;
  readonly keyId: string | null;
  readonly createdAt: Date;
  readonly updatedAt: Date;
}

export interface CapabilityConnectionWriteResult extends CapabilityConnectionMetadata {
  readonly created: boolean;
}

export type CapabilitySecretPayloadV1 = CapabilitySecretPayload;

export class CapabilityConnectionService {
  constructor(
    private readonly prisma: PrismaClient,
    private readonly secrets: LocalSecretEnvelope,
    private readonly readinessProbe: CapabilityReadinessProbe = probeDocmindCredential,
  ) {}

  async rotate(input: RotateCapabilityInput): Promise<CapabilityConnectionWriteResult> {
    if (!CAPABILITY_ID_PATTERN.test(input.capabilityId)) {
      throw new Error(`invalid capabilityId: ${input.capabilityId}`);
    }
    const payload = validateCredential(input);
    await this.prisma.$transaction(async (tx) => {
      await requireCapabilityAdmin(tx, input);
    });
    await this.readinessProbe({
      endpoint: payload.endpoint,
      accessKeyId: payload.accessKeyId,
      accessKeySecret: payload.accessKeySecret,
    });

    return this.prisma.$transaction(async (tx) => {
      await requireCapabilityAdmin(tx, input);
      const connection = await tx.organizationCapabilityConnection.upsert({
        where: {
          organizationId_capabilityId: {
            organizationId: input.organizationId,
            capabilityId: input.capabilityId,
          },
        },
        update: {},
        create: {
          id: randomUUID(),
          organizationId: input.organizationId,
          capabilityId: input.capabilityId,
          status: "DRAFT",
        },
      });
      await tx.$queryRaw`SELECT "id" FROM "OrganizationCapabilityConnection" WHERE "id" = ${connection.id} FOR UPDATE`;
      const locked = await tx.organizationCapabilityConnection.findUniqueOrThrow({
        where: { id: connection.id },
        include: {
          activeSecretVersion: true,
          secretVersions: { orderBy: { version: "desc" }, take: 1, select: { version: true } },
        },
      });
      if (locked.organizationId !== input.organizationId) {
        throw new Error("Capability Connection scope changed during rotation");
      }
      const version = (locked.secretVersions[0]?.version ?? 0) + 1;
      const secretVersionId = randomUUID();
      const envelope = this.secrets.encryptJson(
        {
          purpose: "capability",
          organizationId: input.organizationId,
          connectionId: locked.id,
          secretVersionId,
        },
        payload,
      );
      const now = new Date();
      const secretVersion = await tx.capabilityCredentialVersion.create({
        data: {
          id: secretVersionId,
          connectionId: locked.id,
          version,
          envelopeVersion: envelope.version,
          keyId: envelope.keyId,
          envelope: envelope as unknown as Prisma.InputJsonValue,
          createdByUserId: input.actorUserId,
        },
      });
      if (locked.activeSecretVersion !== null) {
        await tx.capabilityCredentialVersion.update({
          where: { id: locked.activeSecretVersion.id },
          data: { retiredAt: now },
        });
      }
      const activated = await tx.organizationCapabilityConnection.update({
        where: { id: locked.id },
        data: {
          status: "ACTIVE",
          activeSecretVersionId: secretVersion.id,
          activatedAt: now,
          disabledAt: null,
        },
      });
      await tx.auditEntry.create({
        data: {
          organizationId: input.organizationId,
          actorUserId: input.actorUserId,
          action: version === 1 ? "capability.created" : "capability.rotated",
          metadata: {
            connectionId: locked.id,
            capabilityId: input.capabilityId,
            status: "ACTIVE",
            secretVersion: version,
            keyId: envelope.keyId,
          },
        },
      });
      return {
        ...toMetadata(activated, { version, keyId: secretVersion.keyId }),
        created: version === 1,
      };
    });
  }

  async list(organizationId: string): Promise<CapabilityConnectionMetadata[]> {
    const connections = await this.prisma.organizationCapabilityConnection.findMany({
      where: { organizationId },
      include: { activeSecretVersion: { select: { version: true, keyId: true } } },
      orderBy: { capabilityId: "asc" },
    });
    return connections.map((c) => toMetadata(c, c.activeSecretVersion));
  }

  async read(organizationId: string, capabilityId: string): Promise<CapabilityConnectionMetadata | null> {
    const connection = await this.prisma.organizationCapabilityConnection.findFirst({
      where: { organizationId, capabilityId },
      include: { activeSecretVersion: { select: { version: true, keyId: true } } },
    });
    return connection === null ? null : toMetadata(connection, connection.activeSecretVersion);
  }

  async disable(input: {
    readonly organizationId: string;
    readonly capabilityId: string;
    readonly actorUserId: string;
  }): Promise<CapabilityConnectionMetadata> {
    return this.prisma.$transaction(async (tx) => {
      await requireCapabilityAdmin(tx, input);
      const connection = await tx.organizationCapabilityConnection.findFirst({
        where: { organizationId: input.organizationId, capabilityId: input.capabilityId },
        select: { id: true },
      });
      if (connection === null) throw new Error("Capability Connection not found");
      await tx.$queryRaw`SELECT "id" FROM "OrganizationCapabilityConnection" WHERE "id" = ${connection.id} FOR UPDATE`;
      const locked = await tx.organizationCapabilityConnection.findUniqueOrThrow({
        where: { id: connection.id },
        include: { activeSecretVersion: { select: { version: true, keyId: true } } },
      });
      if (locked.status === "DISABLED") return toMetadata(locked, locked.activeSecretVersion);
      const disabled = await tx.organizationCapabilityConnection.update({
        where: { id: locked.id },
        data: { status: "DISABLED", disabledAt: new Date() },
      });
      await tx.auditEntry.create({
        data: {
          organizationId: input.organizationId,
          actorUserId: input.actorUserId,
          action: "capability.disabled",
          metadata: {
            connectionId: locked.id,
            capabilityId: input.capabilityId,
            previousStatus: locked.status,
            status: "DISABLED",
          },
        },
      });
      return toMetadata(disabled, locked.activeSecretVersion);
    });
  }
}

function validateCredential(input: RotateCapabilityInput): CapabilitySecretPayloadV1 {
  if (!KNOWN_CAPABILITY_IDS.has(input.capabilityId)) {
    throw new Error(`unsupported capabilityId: ${input.capabilityId}`);
  }
  const accessKeyId = nonEmpty(input.accessKeyId, "accessKeyId");
  const accessKeySecret = nonEmpty(input.accessKeySecret, "accessKeySecret");
  const endpoint = nonEmpty(input.endpoint, "endpoint");
  return { schemaVersion: 1, accessKeyId, accessKeySecret, endpoint };
}

function toMetadata(
  connection: {
    readonly id: string;
    readonly capabilityId: string;
    readonly status: string;
    readonly createdAt: Date;
    readonly updatedAt: Date;
  },
  secret: { readonly version: number; readonly keyId: string } | null,
): CapabilityConnectionMetadata {
  return {
    id: connection.id,
    capabilityId: connection.capabilityId,
    status: connection.status as CapabilityConnectionMetadata["status"],
    activeVersion: secret?.version ?? null,
    keyId: secret?.keyId ?? null,
    createdAt: connection.createdAt,
    updatedAt: connection.updatedAt,
  };
}

async function requireCapabilityAdmin(
  tx: Prisma.TransactionClient,
  input: { readonly organizationId: string; readonly actorUserId: string },
): Promise<void> {
  await lockActiveOrganization(tx, input.organizationId);
  const membership = await tx.organizationMembership.findFirst({
    where: {
      organizationId: input.organizationId,
      userId: input.actorUserId,
      role: { in: ["OWNER", "ADMIN"] },
      revokedAt: null,
    },
    select: { id: true },
  });
  if (membership === null) {
    throw new Error("only Organization OWNER or ADMIN may manage capability connections");
  }
}

function nonEmpty(value: string, label: string): string {
  const trimmed = value.trim();
  if (trimmed === "") throw new Error(`${label} must not be empty`);
  return trimmed;
}
