import { randomUUID } from "node:crypto";
import type { Prisma, PrismaClient } from "@prisma/client";
import { lockActiveOrganization } from "../org/status.js";
import {
  LocalSecretEnvelope,
  type SecretEnvelopeV1,
} from "../security/secretEnvelope.js";
import { probeOpenRouterCredential, type ProviderReadinessProbe } from "./providerReadiness.js";

const PROVIDER_ID_PATTERN = /^[a-z0-9][a-z0-9._-]{0,63}$/;

export interface ProviderCredentialInput {
  readonly baseUrl: string;
  readonly authToken: string;
  readonly anthropicApiKey?: string;
}

export interface RotateByokProviderInput extends ProviderCredentialInput {
  readonly organizationId: string;
  readonly providerId: string;
  readonly actorUserId: string;
}

export interface ProviderConnectionMetadata {
  readonly id: string;
  readonly providerId: string;
  readonly mode: "BYOK" | "PLATFORM_MANAGED";
  readonly status: "DRAFT" | "ACTIVE" | "DISABLED";
  readonly activeVersion: number | null;
  readonly keyId: string | null;
  readonly createdAt: Date;
  readonly updatedAt: Date;
}

export interface ProviderConnectionWriteResult extends ProviderConnectionMetadata {
  readonly created: boolean;
}

export interface ProviderSecretPayloadV1 {
  readonly schemaVersion: 1;
  readonly baseUrl: string;
  readonly authToken: string;
  readonly anthropicApiKey: string;
}

export class ProviderConnectionService {
  constructor(
    private readonly prisma: PrismaClient,
    private readonly secrets: LocalSecretEnvelope,
    private readonly readinessProbe: ProviderReadinessProbe = probeOpenRouterCredential,
  ) {}

  async rotateByok(input: RotateByokProviderInput): Promise<ProviderConnectionWriteResult> {
    const payload = validateProviderCredential(input);
    await this.prisma.$transaction(async (tx) => {
      await requireByokActor(tx, input);
    });
    await this.readinessProbe({
      providerId: input.providerId,
      baseUrl: payload.baseUrl,
      authToken: payload.authToken,
      anthropicApiKey: payload.anthropicApiKey,
    });

    return this.prisma.$transaction(async (tx) => {
      await requireByokActor(tx, input);
      const connection = await tx.organizationProviderConnection.upsert({
        where: {
          organizationId_providerId: {
            organizationId: input.organizationId,
            providerId: input.providerId,
          },
        },
        update: {},
        create: {
          id: randomUUID(),
          organizationId: input.organizationId,
          providerId: input.providerId,
          mode: "BYOK",
          status: "DRAFT",
        },
      });

      await lockConnection(tx, connection.id);
      const locked = await tx.organizationProviderConnection.findUniqueOrThrow({
        where: { id: connection.id },
        include: {
          activeSecretVersion: { select: { id: true } },
          secretVersions: {
            orderBy: { version: "desc" },
            take: 1,
            select: { version: true },
          },
        },
      });
      if (locked.organizationId !== input.organizationId || locked.providerId !== input.providerId) {
        throw new Error("provider connection scope changed while rotating credential");
      }
      if (locked.mode !== "BYOK") {
        throw new Error(
          "provider connection is managed by platform administrators and cannot be changed through organization API",
        );
      }

      const version = (locked.secretVersions[0]?.version ?? 0) + 1;
      const secretVersionId = randomUUID();
      const envelope = this.secrets.encryptJson({
        purpose: "provider-connection",
        organizationId: input.organizationId,
        connectionId: locked.id,
        secretVersionId,
      }, payload);
      const now = new Date();

      const secretVersion = await tx.providerCredentialVersion.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.providerCredentialVersion.update({
          where: { id: locked.activeSecretVersion.id },
          data: { retiredAt: now },
        });
      }
      const activated = await tx.organizationProviderConnection.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 ? "provider_connection.created" : "provider_connection.rotated",
          metadata: {
            connectionId: locked.id,
            providerId: input.providerId,
            mode: "BYOK",
            status: "ACTIVE",
            secretVersion: version,
            envelopeVersion: envelope.version,
            keyId: envelope.keyId,
          },
        },
      });

      return {
        ...metadata(activated, { version, keyId: secretVersion.keyId }),
        created: version === 1,
      };
    });
  }

  async list(organizationId: string): Promise<readonly ProviderConnectionMetadata[]> {
    return this.prisma.$transaction(async (tx) => {
      await lockActiveOrganization(tx, organizationId);
      const connections = await tx.organizationProviderConnection.findMany({
        where: { organizationId },
        orderBy: [{ providerId: "asc" }, { createdAt: "asc" }],
        include: {
          activeSecretVersion: {
            select: { version: true, keyId: true },
          },
        },
      });
      return connections.map((connection) => metadata(connection, connection.activeSecretVersion));
    });
  }
}

export function decryptStoredProviderCredential(
  secrets: LocalSecretEnvelope,
  input: {
    readonly organizationId: string;
    readonly connectionId: string;
    readonly providerId: string;
    readonly secretVersionId: string;
    readonly envelopeVersion: number;
    readonly keyId: string;
    readonly envelope: Prisma.JsonValue;
  },
): ProviderSecretPayloadV1 {
  if (input.envelopeVersion !== 1) {
    throw new Error(`unsupported provider secret envelope version: ${input.envelopeVersion}`);
  }
  const envelope = input.envelope as unknown as SecretEnvelopeV1;
  if (envelope.keyId !== input.keyId || envelope.version !== input.envelopeVersion) {
    throw new Error("provider secret envelope metadata mismatch");
  }
  const payload = secrets.decryptJson<unknown>({
    purpose: "provider-connection",
    organizationId: input.organizationId,
    connectionId: input.connectionId,
    secretVersionId: input.secretVersionId,
  }, envelope);
  return validateStoredProviderPayload(payload, input.providerId);
}

export async function verifyStoredProviderEnvelopes(
  prisma: PrismaClient | Prisma.TransactionClient,
  secrets: LocalSecretEnvelope,
): Promise<number> {
  const versions = await prisma.providerCredentialVersion.findMany({
    select: {
      id: true,
      envelopeVersion: true,
      keyId: true,
      envelope: true,
      connection: {
        select: { id: true, organizationId: true, providerId: true },
      },
    },
  });
  for (const version of versions) {
    decryptStoredProviderCredential(secrets, {
      organizationId: version.connection.organizationId,
      connectionId: version.connection.id,
      providerId: version.connection.providerId,
      secretVersionId: version.id,
      envelopeVersion: version.envelopeVersion,
      keyId: version.keyId,
      envelope: version.envelope,
    });
  }
  return versions.length;
}

export async function rewrapStoredProviderEnvelopes(
  prisma: Prisma.TransactionClient,
  secrets: LocalSecretEnvelope,
): Promise<{ readonly verified: number; readonly rewrapped: number }> {
  const versions = await prisma.providerCredentialVersion.findMany({
    select: {
      id: true,
      envelopeVersion: true,
      keyId: true,
      envelope: true,
      connection: {
        select: { id: true, organizationId: true, providerId: true },
      },
    },
    orderBy: { createdAt: "asc" },
  });
  let rewrapped = 0;
  for (const version of versions) {
    const binding = {
      purpose: "provider-connection",
      organizationId: version.connection.organizationId,
      connectionId: version.connection.id,
      secretVersionId: version.id,
    } as const;
    decryptStoredProviderCredential(secrets, {
      organizationId: version.connection.organizationId,
      connectionId: version.connection.id,
      providerId: version.connection.providerId,
      secretVersionId: version.id,
      envelopeVersion: version.envelopeVersion,
      keyId: version.keyId,
      envelope: version.envelope,
    });
    if (version.keyId === secrets.activeKeyId) continue;

    const envelope = version.envelope as unknown as SecretEnvelopeV1;
    const nextEnvelope = secrets.rewrap(binding, envelope);
    const updated = await prisma.providerCredentialVersion.updateMany({
      where: { id: version.id, keyId: version.keyId },
      data: {
        keyId: nextEnvelope.keyId,
        envelope: nextEnvelope as unknown as Prisma.InputJsonValue,
      },
    });
    if (updated.count !== 1) {
      throw new Error(`provider secret envelope changed during KEK rotation: ${version.id}`);
    }
    await prisma.auditEntry.create({
      data: {
        organizationId: version.connection.organizationId,
        action: "provider_secret.kek_rewrapped",
        metadata: {
          connectionId: version.connection.id,
          providerId: version.connection.providerId,
          secretVersionId: version.id,
          previousKeyId: version.keyId,
          keyId: nextEnvelope.keyId,
        },
      },
    });
    rewrapped += 1;
  }
  const verified = await verifyStoredProviderEnvelopes(prisma, secrets);
  const remaining = await prisma.providerCredentialVersion.count({
    where: { keyId: { not: secrets.activeKeyId } },
  });
  if (remaining !== 0) {
    throw new Error(`KEK rotation left ${remaining} provider envelope(s) on a non-active key`);
  }
  return { verified, rewrapped };
}

function validateProviderCredential(input: RotateByokProviderInput): ProviderSecretPayloadV1 {
  if (!PROVIDER_ID_PATTERN.test(input.providerId)) {
    throw new Error("invalid provider connection id");
  }
  let baseUrl: URL;
  try {
    baseUrl = new URL(input.baseUrl);
  } catch (error) {
    throw new Error("invalid provider base URL", { cause: error });
  }
  if (baseUrl.protocol !== "https:") {
    throw new Error("invalid provider base URL: HTTPS is required");
  }
  const authToken = input.authToken.trim();
  if (authToken === "") {
    throw new Error("provider auth token is required");
  }
  return {
    schemaVersion: 1,
    baseUrl: baseUrl.toString().replace(/\/$/, ""),
    authToken,
    anthropicApiKey: input.anthropicApiKey?.trim() ?? "",
  };
}

function metadata(
  connection: {
    readonly id: string;
    readonly providerId: string;
    readonly mode: "BYOK" | "PLATFORM_MANAGED";
    readonly status: "DRAFT" | "ACTIVE" | "DISABLED";
    readonly createdAt: Date;
    readonly updatedAt: Date;
  },
  secret: { readonly version: number; readonly keyId: string } | null,
): ProviderConnectionMetadata {
  return {
    id: connection.id,
    providerId: connection.providerId,
    mode: connection.mode,
    status: connection.status,
    activeVersion: secret?.version ?? null,
    keyId: secret?.keyId ?? null,
    createdAt: connection.createdAt,
    updatedAt: connection.updatedAt,
  };
}

function validateStoredProviderPayload(value: unknown, providerId: string): ProviderSecretPayloadV1 {
  if (!isRecord(value) || value["schemaVersion"] !== 1 || typeof value["baseUrl"] !== "string" ||
      typeof value["authToken"] !== "string" || typeof value["anthropicApiKey"] !== "string" ||
      value["authToken"].trim() === "") {
    throw new Error(`invalid encrypted provider credential payload: ${providerId}`);
  }
  let baseUrl: URL;
  try {
    baseUrl = new URL(value["baseUrl"]);
  } catch (error) {
    throw new Error(`invalid encrypted provider credential payload: ${providerId}`, { cause: error });
  }
  if (baseUrl.protocol !== "https:") {
    throw new Error(`invalid encrypted provider credential payload: ${providerId}`);
  }
  return {
    schemaVersion: 1,
    baseUrl: value["baseUrl"],
    authToken: value["authToken"],
    anthropicApiKey: value["anthropicApiKey"],
  };
}

function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === "object" && value !== null && !Array.isArray(value);
}

async function lockConnection(tx: Prisma.TransactionClient, connectionId: string): Promise<void> {
  await tx.$queryRaw`SELECT "id" FROM "OrganizationProviderConnection" WHERE "id" = ${connectionId} FOR UPDATE`;
}

async function requireByokActor(
  tx: Prisma.TransactionClient,
  input: Pick<RotateByokProviderInput, "organizationId" | "providerId" | "actorUserId">,
): Promise<void> {
  await lockActiveOrganization(tx, input.organizationId);
  const [authorizedActor, existingConnection] = await Promise.all([
    tx.organizationMembership.findFirst({
      where: {
        organizationId: input.organizationId,
        userId: input.actorUserId,
        role: { in: ["OWNER", "ADMIN"] },
        revokedAt: null,
      },
      select: { id: true },
    }),
    tx.organizationProviderConnection.findUnique({
      where: {
        organizationId_providerId: {
          organizationId: input.organizationId,
          providerId: input.providerId,
        },
      },
      select: { mode: true },
    }),
  ]);
  if (authorizedActor === null) {
    throw new Error("BYOK provider rotation requires an active Organization OWNER or ADMIN");
  }
  if (existingConnection?.mode === "PLATFORM_MANAGED") {
    throw new Error(
      "provider connection is managed by platform administrators and cannot be changed through organization API",
    );
  }
}
