import { CUSTODY_PROVIDERS, type CustodyProvider, redactCredentialString } from "@sdp/custody";
import { normalizePem } from "@sdp/custody/provisioning";
import { SigningError } from "@sdp/custody/signing";
import { z } from "zod";
import { getDb } from "@/db";
import { AppError, badRequest, conflict, forbidden } from "@/lib/errors";
import { isCustodyConnectionRuntimeEnabled } from "@/lib/feature-flags";
import { created, success } from "@/lib/response";
import { getRequestTenantScope } from "@/lib/tenant-scope";
import { clearWalletCaches } from "@/routes/custody/handlers/wallets";
import { AuditService } from "@/services/audit.service";
import { provisionFireblocksVaultAccount } from "@/services/custody/provisioning";
import {
  type FireblocksProviderConfig,
  parseConfigRecord,
} from "@/services/domain/signing/provider-config";
import { createSigningService } from "@/services/domain/signing.service";
import {
  assertProviderAvailable,
  getEnabledProviders,
  getProviderAvailability,
} from "@/services/provider-availability.service";
import { type AppContext, getPreferredWalletForConfig, resolveActor } from "../context";
import {
  type InitializeSigningRequest,
  type InitializeSigningResponse,
  initializeSigningSchema,
  type SwitchProviderOptionsResponse,
  type SwitchSigningRequest,
  switchSigningSchema,
} from "../schemas";

type SigningInitializationResult = {
  configId: string;
  publicKey: string;
  walletId: string;
};

const EXISTING_PROVIDER_OBJECT_SELECTORS: Partial<Record<CustodyProvider, readonly string[]>> = {
  coinbase_cdp: ["walletAddress"],
  para: ["walletId"],
  turnkey: ["privateKeyId"],
  dfns: ["walletId", "signingKeyId"],
  ibm_haven: ["walletId", "signingKeyId"],
  anchorage: ["walletId"],
};

export function assertNoExistingProviderObjectSelector(body: unknown): void {
  if (!body || typeof body !== "object" || Array.isArray(body)) {
    return;
  }

  const request = body as Record<string, unknown>;
  if (typeof request.provider !== "string") {
    return;
  }

  if (!Object.hasOwn(EXISTING_PROVIDER_OBJECT_SELECTORS, request.provider)) {
    return;
  }

  const selectors = EXISTING_PROVIDER_OBJECT_SELECTORS[request.provider as CustodyProvider] ?? [];
  const suppliedSelector = selectors.find((selector) => Object.hasOwn(request, selector));
  if (suppliedSelector) {
    throw badRequest(
      `${suppliedSelector} cannot select an existing wallet when using platform-managed provider credentials`
    );
  }
}

export const initializeSigning = async (c: AppContext) => {
  const actor = resolveActor(c);
  const projectId = c.get("projectId");

  const body = await c.req.json();
  assertNoExistingProviderObjectSelector(body);
  const parsed = initializeSigningSchema.safeParse(body);

  if (!parsed.success) {
    throw badRequest("Invalid request body", {
      errors: z.flattenError(parsed.error).fieldErrors,
    });
  }

  const signingService = createSigningService(c.env, getRequestTenantScope(c));

  try {
    const result = await initializeProviderConnection(
      c,
      signingService,
      c.env,
      actor.organizationId,
      await resolveOrganizationSlug(c, actor.organizationId),
      projectId,
      parsed.data
    );

    const auditService = new AuditService(getDb(c.env));
    await auditService.log(c, {
      action: "create",
      resourceType: "custody_config",
      resourceId: result.configId,
      metadata: {
        event: "provider_connected",
        provider: parsed.data.provider,
        projectId: projectId ?? null,
      },
    });

    clearWalletCaches();

    return created(c, toInitializeSigningResponse(result));
  } catch (error) {
    handleSigningInitializationError(error);
  }
};

export const switchSigning = async (c: AppContext) => {
  const actor = resolveActor(c);

  const body = await c.req.json();
  assertNoExistingProviderObjectSelector(body);
  const parsed = switchSigningSchema.safeParse(body);

  if (!parsed.success) {
    throw badRequest("Invalid request body", {
      errors: z.flattenError(parsed.error).fieldErrors,
    });
  }

  const signingService = createSigningService(c.env, getRequestTenantScope(c));
  const auditService = new AuditService(getDb(c.env));
  const projectId = c.get("projectId");
  const targetProvider = parsed.data.provider;

  const existingScopeConfig = await findScopeConfigByProvider(
    c,
    actor.organizationId,
    projectId,
    targetProvider
  );

  try {
    let result: SigningInitializationResult;

    if (existingScopeConfig?.status === "active") {
      await assertProviderAvailable(
        c.env,
        getDb(c.env),
        actor.organizationId,
        "custody",
        targetProvider
      );
      await signingService.setDefaultConfiguration(
        actor.organizationId,
        projectId,
        existingScopeConfig.id
      );
      result = await getActiveConfigInitializationResult(
        c,
        existingScopeConfig.id,
        existingScopeConfig.default_wallet_id
      );

      await logDefaultProviderChanged(c, auditService, existingScopeConfig.id, {
        projectId,
        provider: targetProvider,
      });
    } else {
      result = await initializeProviderConnection(
        c,
        signingService,
        c.env,
        actor.organizationId,
        await resolveOrganizationSlug(c, actor.organizationId),
        projectId,
        parsed.data
      );

      await signingService.setDefaultConfiguration(
        actor.organizationId,
        projectId,
        result.configId
      );

      const wasReactivated =
        existingScopeConfig?.status === "inactive" && existingScopeConfig.id === result.configId;

      await auditService.log(c, {
        action: wasReactivated ? "update" : "create",
        resourceType: "custody_config",
        resourceId: result.configId,
        metadata: {
          event: wasReactivated ? "provider_reactivated" : "provider_connected",
          provider: targetProvider,
          projectId: projectId ?? null,
        },
      });

      await logDefaultProviderChanged(c, auditService, result.configId, {
        projectId,
        provider: targetProvider,
      });
    }

    clearWalletCaches();

    return created(c, toInitializeSigningResponse(result));
  } catch (error) {
    handleSigningInitializationError(error);
  }
};

export const getSwitchProviderOptions = async (c: AppContext) => {
  const actor = resolveActor(c);
  const projectId = c.get("projectId");
  const signingService = createSigningService(c.env, getRequestTenantScope(c));
  const enabledProviders = (await getEnabledProviders(c.env, getDb(c.env), actor.organizationId))
    .custody;
  const [reuseState, configurations] = await Promise.all([
    signingService.getProviderReuseState(actor.organizationId, projectId),
    signingService.getConfigurations(actor.organizationId, projectId),
  ]);

  const activeProviders = new Set(configurations.configs.map((config) => config.provider));
  const defaultProvider =
    configurations.configs.find((config) => config.id === configurations.defaultConfigId)
      ?.provider ?? null;

  const response: SwitchProviderOptionsResponse = {
    providers: CUSTODY_PROVIDERS.filter((provider) => enabledProviders.includes(provider)).map(
      (provider) => {
        const hasReusableWallet =
          provider === "privy"
            ? reuseState.privy
            : provider === "coinbase_cdp"
              ? reuseState.coinbase_cdp
              : provider === "para"
                ? reuseState.para
                : provider === "turnkey"
                  ? reuseState.turnkey
                  : provider === "utila"
                    ? reuseState.utila
                    : false;

        const needsWalletLabel =
          provider === "fireblocks" ? false : provider === "local" ? true : !hasReusableWallet;

        return {
          provider,
          hasReusableWallet,
          needsWalletLabel,
          isActive: activeProviders.has(provider),
          isDefault: defaultProvider === provider,
        };
      }
    ),
  };

  return success(c, response);
};

async function initializeProviderConnection(
  c: AppContext,
  signingService: ReturnType<typeof createSigningService>,
  env: AppContext["env"],
  organizationId: string,
  organizationSlug: string,
  projectId: string | undefined,
  request: InitializeSigningRequest | SwitchSigningRequest
): Promise<SigningInitializationResult> {
  if (request.provider === "privy") {
    await assertFreshPrivyLegacySetupAllowed(c, organizationId, projectId);
  }
  await assertProviderAvailable(env, getDb(c.env), organizationId, "custody", request.provider);

  switch (request.provider) {
    case "local":
      return signingService.initializeLocalSigning(organizationId, projectId, {
        walletLabel: request.walletLabel,
      });
    case "fireblocks": {
      if (!env.FIREBLOCKS_API_KEY || !env.FIREBLOCKS_API_SECRET) {
        throw badRequest("Fireblocks backend credentials are not configured");
      }

      const resolvedApiKey = env.FIREBLOCKS_API_KEY;
      const resolvedApiSecretPem = normalizePem(env.FIREBLOCKS_API_SECRET);
      const existingFireblocksConfig = await findScopeFireblocksConfig(
        c,
        organizationId,
        projectId
      );

      const { vaultAccountId, assetId } = existingFireblocksConfig
        ? {
            vaultAccountId: existingFireblocksConfig.vaultAccountId,
            assetId: existingFireblocksConfig.assetId,
          }
        : await provisionFireblocksVaultAccount(env, {
            orgId: organizationId,
            orgSlug: organizationSlug,
            apiKey: resolvedApiKey,
            apiSecretPem: env.FIREBLOCKS_API_SECRET,
          });

      return signingService.initializeFireblocksSigning(organizationId, projectId, {
        apiKey: resolvedApiKey,
        apiSecretPem: resolvedApiSecretPem,
        vaultAccountId,
        assetId,
        walletLabel: request.walletLabel,
      });
    }
    case "privy":
      return signingService.initializePrivySigning(organizationId, projectId, {
        requestDelayMs: request.requestDelayMs,
        walletLabel: request.walletLabel,
      });
    case "coinbase_cdp":
      return signingService.initializeCoinbaseCdpSigning(organizationId, projectId, {
        network: request.network,
        accountPolicy: request.accountPolicy,
        walletLabel: request.walletLabel,
      });
    case "para":
      return signingService.initializeParaSigning(organizationId, projectId, {
        requestDelayMs: request.requestDelayMs,
        walletLabel: request.walletLabel,
      });
    case "turnkey":
      return signingService.initializeTurnkeySigning(organizationId, projectId, {
        requestDelayMs: request.requestDelayMs,
        walletLabel: request.walletLabel,
      });
    case "dfns":
      return signingService.initializeDfnsSigning(organizationId, projectId, {
        network: request.network,
        walletLabel: request.walletLabel,
      });
    case "ibm_haven":
      return signingService.initializeIbmHavenSigning(organizationId, projectId, {
        network: request.network,
        walletLabel: request.walletLabel,
      });
    case "anchorage":
      return signingService.initializeAnchorageWalletLifecycle(organizationId, projectId, {
        walletLabel: request.walletLabel,
        network: request.network,
      });
    case "utila":
      return signingService.initializeUtilaSigning(organizationId, projectId, {
        walletLabel: request.walletLabel,
      });
    default:
      throw badRequest("Unsupported provider");
  }
}

async function assertFreshPrivyLegacySetupAllowed(
  c: AppContext,
  organizationId: string,
  projectId: string | undefined
): Promise<void> {
  if (!projectId) {
    throw badRequest("Project scope is required");
  }

  if (!isCustodyConnectionRuntimeEnabled(c.env, "privy")) {
    return;
  }

  const blockingConnection = await getDb(c.env)
    .prepare(
      `SELECT id
       FROM custody_connections
       WHERE organization_id = ?
         AND project_id = ?
         AND provider = 'privy'
         AND status IN ('pending', 'checking', 'active')
       LIMIT 1`
    )
    .bind(organizationId, projectId)
    .first<{ id: string }>();
  if (blockingConnection) {
    throw conflict("Privy custody setup already exists for this project");
  }

  const existingConfig = await findScopeConfigByProvider(c, organizationId, projectId, "privy");
  if (existingConfig?.status === "active") {
    return;
  }

  const availability = await getProviderAvailability(c.env, getDb(c.env), organizationId);
  if (availability.providers.custody.privy.entitled) {
    throw forbidden("New Privy setup must use stored credentials");
  }
}

async function getActiveConfigInitializationResult(
  c: AppContext,
  configId: string,
  defaultWalletId: string | null
): Promise<SigningInitializationResult> {
  const preferredWallet = await getPreferredWalletForConfig(
    getDb(c.env),
    configId,
    defaultWalletId
  );
  if (!preferredWallet) {
    throw conflict("Active provider is missing an active wallet");
  }
  return {
    configId,
    publicKey: preferredWallet.publicKey,
    walletId: preferredWallet.walletId,
  };
}

async function findScopeConfigByProvider(
  c: AppContext,
  organizationId: string,
  projectId: string | undefined,
  provider: CustodyProvider
): Promise<{
  id: string;
  status: "active" | "inactive";
  default_wallet_id: string | null;
} | null> {
  return getDb(c.env)
    .prepare(
      projectId
        ? `SELECT id, status, default_wallet_id
           FROM custody_configs
           WHERE organization_id = ? AND project_id = ? AND provider = ?
           LIMIT 1`
        : `SELECT id, status, default_wallet_id
           FROM custody_configs
           WHERE organization_id = ? AND project_id IS NULL AND provider = ?
           LIMIT 1`
    )
    .bind(...(projectId ? [organizationId, projectId, provider] : [organizationId, provider]))
    .first<{
      id: string;
      status: "active" | "inactive";
      default_wallet_id: string | null;
    }>();
}

async function findScopeProviderConfigRecord(
  c: AppContext,
  organizationId: string,
  projectId: string | undefined,
  provider: CustodyProvider
) {
  return getDb(c.env)
    .prepare(
      projectId
        ? `SELECT id,
                organization_id,
                project_id,
                provider,
                config_encrypted AS config,
                encryption_version,
                default_wallet_id,
                status,
                created_at,
                updated_at
           FROM custody_configs
           WHERE organization_id = ? AND project_id = ? AND provider = ?
           LIMIT 1`
        : `SELECT id,
                organization_id,
                project_id,
                provider,
                config_encrypted AS config,
                encryption_version,
                default_wallet_id,
                status,
                created_at,
                updated_at
           FROM custody_configs
           WHERE organization_id = ? AND project_id IS NULL AND provider = ?
           LIMIT 1`
    )
    .bind(...(projectId ? [organizationId, projectId, provider] : [organizationId, provider]))
    .first<{
      id: string;
      organization_id: string;
      project_id: string | null;
      provider: CustodyProvider;
      config: string;
      encryption_version: string;
      default_wallet_id: string | null;
      status: "active" | "inactive";
      created_at: string;
      updated_at: string;
    }>();
}

async function findScopeFireblocksConfig(
  c: AppContext,
  organizationId: string,
  projectId: string | undefined
): Promise<FireblocksProviderConfig | null> {
  const record = await findScopeProviderConfigRecord(c, organizationId, projectId, "fireblocks");
  if (!record) {
    return null;
  }

  const parsed = await parseConfigRecord(c.env, organizationId, {
    id: record.id,
    organizationId: record.organization_id,
    projectId: record.project_id,
    provider: record.provider,
    config: record.config,
    encryptionVersion: record.encryption_version,
    defaultWalletId: record.default_wallet_id,
    status: record.status,
    createdAt: record.created_at,
    updatedAt: record.updated_at,
  });

  return parsed.provider === "fireblocks" ? parsed : null;
}

async function resolveOrganizationSlug(c: AppContext, organizationId: string): Promise<string> {
  const row = await getDb(c.env)
    .prepare("SELECT slug FROM organizations WHERE id = ? LIMIT 1")
    .bind(organizationId)
    .first<{ slug: string | null }>();

  return row?.slug?.trim() || organizationId;
}

async function logDefaultProviderChanged(
  c: AppContext,
  auditService: AuditService,
  resourceId: string,
  params: {
    projectId: string | undefined;
    provider: CustodyProvider;
  }
): Promise<void> {
  await auditService.log(c, {
    action: "update",
    resourceType: "custody_config",
    resourceId,
    metadata: {
      event: "default_provider_changed",
      provider: params.provider,
      projectId: params.projectId ?? null,
    },
  });
}

function toInitializeSigningResponse(
  result: SigningInitializationResult
): InitializeSigningResponse {
  return {
    configId: result.configId,
    publicKey: result.publicKey,
    walletId: result.walletId,
  };
}

function handleSigningInitializationError(error: unknown): never {
  if (error instanceof SigningError) {
    if (error.code === "ALREADY_INITIALIZED") {
      throw new AppError("CONFLICT", redactCredentialString(error.message));
    }
    if (error.code === "NETWORK_ERROR" || error.code === "PROVIDER_NOT_CONFIGURED") {
      throw badRequest("Provider setup failed. Check provider configuration and try again.");
    }
    throw badRequest(redactCredentialString(error.message));
  }

  throw error;
}
