import type { Prisma, PrismaClient } from "@prisma/client";
import { InMemoryModelRegistry, type ModelRegistry, type RoleEntry, type RoleSkillEntry } from "../agent/models.js";
import { lockActiveOrganization } from "../org/status.js";
import { decryptStoredProviderCredential } from "../connections/providerConnections.js";
import { openProviderProxyLease, type AgentProviderLease, type ProviderUpstreamCredential } from "../connections/providerProxy.js";
import { LocalSecretEnvelope } from "../security/secretEnvelope.js";

export type Env = Readonly<Record<string, string | undefined>>;

type EnvSource = Env | (() => Env);

const DEFAULT_SONNET_MODEL = "anthropic/claude-sonnet-5";
const DEFAULT_SONNET_LABEL = "Claude Sonnet 5";
const DEFAULT_AGENT_MAX_TURNS = 25;
const DEFAULT_AGENT_MAX_CONCURRENT_RUNS = 1;
const DEFAULT_AGENT_MAX_RUN_SECONDS = 900;

export interface ProviderRuntimeSettings {
  readonly id: string;
  openAgentLease(input: { readonly runId: string }): Promise<AgentProviderLease>;
}

export interface ProviderLeaseContext {
  readonly providerId: string;
  readonly projectId: string;
  readonly runId: string;
}

export type ProviderLeaseFactory = (
  credential: ProviderUpstreamCredential,
  context: ProviderLeaseContext,
) => Promise<AgentProviderLease>;

const DEFAULT_PROVIDER_LEASE_FACTORY: ProviderLeaseFactory = (credential) =>
  openProviderProxyLease(credential);

export interface RuntimeScope {
  readonly projectId?: string;
}

export interface RunPolicyInput {
  readonly projectId: string;
  readonly roleId: string;
}

export interface RunPolicy {
  readonly maxTurns: number;
  readonly maxConcurrentRuns: number;
  readonly maxRunSeconds: number;
}

export interface RuntimeSettings {
  provider(providerId: string, scope?: RuntimeScope): Promise<ProviderRuntimeSettings>;
  modelRegistry(scope?: RuntimeScope): Promise<ModelRegistry>;
  runPolicy(input: RunPolicyInput): Promise<RunPolicy>;
}

export class EnvRuntimeSettings implements RuntimeSettings {
  private readonly envSource: () => Env;

  constructor(env: EnvSource = process.env) {
    this.envSource = typeof env === "function" ? env : () => env;
  }

  async provider(providerId: string, scope?: RuntimeScope): Promise<ProviderRuntimeSettings> {
    void providerId;
    void scope;
    throw new Error(
      "process-global provider credentials are disabled; use the project-scoped provider resolver",
    );
  }

  async modelRegistry(scope?: RuntimeScope): Promise<ModelRegistry> {
    void scope;
    return createDefaultModelRegistry(this.envSource());
  }

  async runPolicy(input: RunPolicyInput): Promise<RunPolicy> {
    void input;
    return {
      maxTurns: positiveIntegerEnv(this.envSource(), "HUB_AGENT_MAX_TURNS", DEFAULT_AGENT_MAX_TURNS),
      maxConcurrentRuns: positiveIntegerEnv(
        this.envSource(),
        "HUB_AGENT_MAX_CONCURRENT_RUNS",
        DEFAULT_AGENT_MAX_CONCURRENT_RUNS,
      ),
      maxRunSeconds: positiveIntegerEnv(
        this.envSource(),
        "HUB_AGENT_MAX_RUN_SECONDS",
        DEFAULT_AGENT_MAX_RUN_SECONDS,
      ),
    };
  }
}

export class DatabaseRuntimeSettings implements RuntimeSettings {
  private readonly envSettings: EnvRuntimeSettings;

  constructor(
    private readonly prisma: PrismaClient,
    private readonly secrets: LocalSecretEnvelope,
    env: EnvSource = process.env,
    private readonly leaseFactory: ProviderLeaseFactory = DEFAULT_PROVIDER_LEASE_FACTORY,
  ) {
    this.envSettings = new EnvRuntimeSettings(env);
  }

  async provider(providerId: string, scope?: RuntimeScope): Promise<ProviderRuntimeSettings> {
    const projectId = scope?.projectId?.trim();
    if (projectId === undefined || projectId === "") {
      throw new Error("projectId is required to resolve provider credentials");
    }

    await this.prisma.$transaction(async (tx) => {
      await loadActiveProviderSecret(tx, projectId, providerId);
    });

    return {
      id: providerId,
      openAgentLease: async ({ runId }) => this.prisma.$transaction(async (tx) => {
        const resolved = await loadActiveProviderSecret(tx, projectId, providerId);
        const secretVersion = resolved.connection.activeSecretVersion;
        const credential = decryptStoredProviderCredential(this.secrets, {
          organizationId: resolved.organizationId,
          connectionId: resolved.connection.id,
          providerId,
          secretVersionId: secretVersion.id,
          envelopeVersion: secretVersion.envelopeVersion,
          keyId: secretVersion.keyId,
          envelope: secretVersion.envelope,
        });
        return this.leaseFactory(
          {
            baseUrl: credential.baseUrl,
            authToken: credential.authToken,
            anthropicApiKey: credential.anthropicApiKey,
          },
          { providerId, projectId, runId },
        );
      }),
    };
  }

  async modelRegistry(scope?: RuntimeScope): Promise<ModelRegistry> {
    const projectId = scope?.projectId?.trim();
    if (projectId === undefined || projectId === "") {
      throw new Error("projectId is required to resolve Agent runtime configuration");
    }
    const project = await this.prisma.project.findUnique({
      where: { id: projectId },
      select: {
        archivedAt: true,
        organization: {
          select: {
            id: true,
            status: true,
            agentRoles: {
              where: { disabledAt: null },
              orderBy: [{ sortOrder: "asc" }, { roleId: "asc" }],
              include: {
                skillBindings: {
                  orderBy: [{ sortOrder: "asc" }, { agentSkillId: "asc" }],
                  include: { skill: true },
                },
              },
            },
          },
        },
      },
    });
    if (project === null || project.archivedAt !== null) {
      throw new Error(`active project not found: ${projectId}`);
    }
    if (project.organization.status !== "ACTIVE") {
      throw new Error(`organization ${project.organization.id} is ${project.organization.status}`);
    }
    if (project.organization.agentRoles.length === 0) {
      throw new Error(`no active Agent roles configured for organization ${project.organization.id}`);
    }
    const defaultRoles = project.organization.agentRoles.filter((role) => role.isDefault);
    if (defaultRoles.length !== 1) {
      throw new Error(`organization ${project.organization.id} must have exactly one active default Agent role`);
    }

    // The model list is the env-based fallback used when a role has no
    // explicit defaultModel. Role defaultModels are chosen from the provider
    // model catalog (admin surface) and may not be in the env list — we don't
    // validate against it at runtime; the admin already validated by selection.
    const defaults = await this.envSettings.modelRegistry(scope);
    const fallbackModels = defaults.listModels();
    const roles: RoleEntry[] = project.organization.agentRoles.map((role) => ({
      id: role.roleId,
      label: role.label,
      defaultModel: role.defaultModel ?? undefined,
      ...(role.systemPrompt !== null ? { systemPrompt: role.systemPrompt } : {}),
      ...(role.tools !== null ? { tools: roleToolsFromJson(role.roleId, role.tools) } : {}),
      skills: role.skillBindings.map((binding): RoleSkillEntry => {
        if (binding.skill.disabledAt !== null) {
          throw new Error(`role ${role.roleId} selects disabled skill ${binding.skill.name}`);
        }
        if (!/^[a-f0-9]{64}$/.test(binding.skill.contentDigest)) {
          throw new Error(`skill ${binding.skill.name} has invalid content digest`);
        }
        if (!/^[a-z0-9][a-z0-9-]{0,63}$/.test(binding.skill.name)) {
          throw new Error(`skill has invalid name: ${binding.skill.name}`);
        }
        if (binding.skill.version.trim() === "") {
          throw new Error(`skill ${binding.skill.name} has empty version`);
        }
        return {
          name: binding.skill.name,
          version: binding.skill.version,
          contentDigest: binding.skill.contentDigest,
        };
      }),
    }));
    return new InMemoryModelRegistry(fallbackModels, roles);
  }

  runPolicy(input: RunPolicyInput): Promise<RunPolicy> {
    return this.envSettings.runPolicy(input);
  }
}

function roleToolsFromJson(roleId: string, value: Prisma.JsonValue): readonly string[] {
  if (!Array.isArray(value) || value.some((tool) => typeof tool !== "string")) {
    throw new Error(`role ${roleId} tools must be a JSON string array`);
  }
  return value as string[];
}


async function loadActiveProviderSecret(
  tx: Prisma.TransactionClient,
  projectId: string,
  providerId: string,
): Promise<{
  readonly organizationId: string;
  readonly connection: {
    readonly id: string;
    readonly activeSecretVersion: {
      readonly id: string;
      readonly connectionId: string;
      readonly envelopeVersion: number;
      readonly keyId: string;
      readonly envelope: Prisma.JsonValue;
      readonly retiredAt: Date | null;
    };
  };
}> {
  const project = await tx.project.findUnique({
    where: { id: projectId },
    select: { organizationId: true, archivedAt: true },
  });
  if (project === null || project.archivedAt !== null) {
    throw new Error(`active project not found: ${projectId}`);
  }
  await lockActiveOrganization(tx, project.organizationId);

  const connection = await tx.organizationProviderConnection.findUnique({
    where: {
      organizationId_providerId: {
        organizationId: project.organizationId,
        providerId,
      },
    },
    include: { activeSecretVersion: true },
  });
  const activeSecretVersion = connection?.activeSecretVersion;
  if (connection === null || connection.status !== "ACTIVE" ||
      activeSecretVersion === null || activeSecretVersion === undefined || activeSecretVersion.retiredAt !== null ||
      activeSecretVersion.connectionId !== connection.id) {
    throw new Error(`active provider connection not found: ${providerId} for project ${projectId}`);
  }
  return {
    organizationId: project.organizationId,
    connection: { id: connection.id, activeSecretVersion },
  };
}

export function createEnvRuntimeSettings(env: EnvSource = process.env): RuntimeSettings {
  return new EnvRuntimeSettings(env);
}

export function createDatabaseRuntimeSettings(
  prisma: PrismaClient,
  secrets: LocalSecretEnvelope,
  env: EnvSource = process.env,
  leaseFactory: ProviderLeaseFactory = DEFAULT_PROVIDER_LEASE_FACTORY,
): RuntimeSettings {
  return new DatabaseRuntimeSettings(prisma, secrets, env, leaseFactory);
}

export function defaultSonnetModel(env: Env = process.env): string {
  return readEnv(env, "ANTHROPIC_DEFAULT_SONNET_MODEL") ?? DEFAULT_SONNET_MODEL;
}

export function createDefaultModelRegistry(env: Env = process.env): InMemoryModelRegistry {
  const sonnetModel = defaultSonnetModel(env);
  const sonnetLabel =
    readEnv(env, "ANTHROPIC_DEFAULT_SONNET_MODEL_NAME") ??
    (sonnetModel === DEFAULT_SONNET_MODEL ? DEFAULT_SONNET_LABEL : sonnetModel);

  return new InMemoryModelRegistry(
    [
      { id: sonnetModel, label: sonnetLabel, toolCapable: true },
    ],
    [
      { id: "draft", label: "草稿", defaultModel: sonnetModel },
      { id: "review", label: "审校", defaultModel: sonnetModel },
    ],
  );
}

function readEnv(env: Env, name: string): string | undefined {
  const value = env[name]?.trim();
  return value === undefined || value === "" ? undefined : value;
}

function positiveIntegerEnv(env: Env, name: string, fallback: number): number {
  const raw = readEnv(env, name);
  if (raw === undefined) return fallback;
  const value = Number(raw);
  if (!Number.isInteger(value) || value <= 0) {
    throw new Error(`runtime setting ${name} must be a positive integer`);
  }
  return value;
}
