/**
 * API Key Service
 *
 * Shared data access for API key operations.
 */

// biome-ignore-all lint/security/noSecrets: service operation identifiers are not credentials
import { hashString } from "@sdp/payments/hash";
import type {
  ApiKeyEnvironment,
  ApiKeyRole,
  ApiKeyStatus,
  ApiKeyWalletScope,
  Permission,
} from "@sdp/types";
import type { DatabaseExecutor } from "@/db";
import { parseOptionalPostgresJson, parsePostgresJson } from "@/db/postgres-utils";
import type { ApiKeyWalletPolicyBindingRow } from "@/db/repositories";
import { AppError, badRequest, internalError } from "@/lib/errors";
import { assertTenantClaim, type TenantScope, TenantScopeViolationError } from "@/lib/tenant-scope";
import { createApiKeyMaterial } from "./api-key.utils";
import { assertGrantableApiKeyPermissions } from "./api-key-scope.service";

export interface ApiKeyListItem {
  id: string;
  name: string;
  description: string | null;
  keyPrefix: string;
  role: ApiKeyRole;
  environment: ApiKeyEnvironment;
  status: ApiKeyStatus;
  walletScope: ApiKeyWalletScope;
  signingWalletId: string | null;
  lastUsedAt: string | null;
  expiresAt: string | null;
  createdAt: string;
}

export interface ApiKeyDetails extends ApiKeyListItem {
  projectId: string;
  allowedIps: string[] | null;
  permissions: Permission[] | null;
  signingWalletId: string | null;
  rotatedFrom: string | null;
  rotationDeadline: string | null;
}

export interface CreateApiKeyInput {
  organizationId: string;
  projectId: string;
  createdByKeyId?: string;
  createdByUserId?: string;
  actorPermissions: Permission[];
  name: string;
  description?: string | null;
  role: ApiKeyRole;
  permissions?: Permission[] | null;
  allowedIps?: string[] | null;
  expiresAt?: string | null;
  signingWalletId?: string | null;
  pepper?: string;
}

export interface UpdateApiKeyInput {
  keyId: string;
  organizationId: string;
  projectId: string;
  actorPermissions: Permission[];
  currentRole: ApiKeyRole;
  name?: string;
  description?: string | null;
  allowedIps?: string[] | null;
  expiresAt?: string | null;
  permissions?: Permission[] | null;
  signingWallet?: { walletId: string | null };
}

export interface CreateApiKeyResult {
  id: string;
  name: string;
  key: string;
  keyPrefix: string;
  role: ApiKeyRole;
  environment: ApiKeyEnvironment;
  expiresAt: string | null;
  createdAt: string;
  keyHash: string;
}

export interface RotateApiKeyResult {
  apiKey: {
    id: string;
    name: string;
    key: string;
    keyPrefix: string;
    role: ApiKeyRole;
    environment: ApiKeyEnvironment;
    expiresAt: string | null;
    createdAt: string;
  };
  previousKey: {
    id: string;
    rotationDeadline: string;
  };
  previousKeyHash: string;
}

export interface VerifyApiKeyOwnershipInput {
  apiKey: string;
  organizationId: string;
  projectId: string;
  pepper?: string;
}

interface ApiKeyListRow {
  id: string;
  name: string;
  description: string | null;
  key_prefix: string;
  role: ApiKeyRole;
  environment: ApiKeyEnvironment;
  status: ApiKeyStatus;
  wallet_scope: ApiKeyWalletScope;
  signing_wallet_id: string | null;
  last_used_at: string | null;
  expires_at: string | null;
  created_at: string;
}

interface ApiKeyDetailsRow extends ApiKeyListRow {
  project_id: string;
  allowed_ips: string | null;
  permissions: string | null;
  signing_wallet_id: string | null;
  rotated_from: string | null;
  rotation_deadline: string | null;
}

function stringifyJsonb(value: unknown, fallback: unknown): string {
  if (typeof value === "string") {
    return value;
  }

  return JSON.stringify(value ?? fallback);
}

export class ApiKeyService {
  constructor(
    private db: DatabaseClient,
    private scope: TenantScope
  ) {
    if (!scope.projectId) {
      throw new TenantScopeViolationError("ApiKeyService requires a project tenant scope");
    }
  }

  /**
   * Verifies full API-key material against the current tenant boundary.
   *
   * Display prefixes are intentionally not used here: they are short, public,
   * and not unique. The raw key is hashed in memory and is never stored.
   */
  async ownsUsableApiKey(input: VerifyApiKeyOwnershipInput): Promise<boolean> {
    assertTenantClaim(this.scope, input, "ApiKeyService.ownsUsableApiKey");
    const keyHash = await hashString(input.apiKey, input.pepper);
    const row = await this.db
      .prepare(
        `SELECT status, expires_at
         FROM api_keys
         WHERE key_hash = ? AND organization_id = ? AND project_id = ?
         LIMIT 1`
      )
      .bind(keyHash, input.organizationId, input.projectId)
      .first<{ status: ApiKeyStatus; expires_at: string | null }>();

    if (row?.status !== "active") {
      return false;
    }

    return !row.expires_at || new Date(row.expires_at) >= new Date();
  }

  async listForProject(projectId: string): Promise<ApiKeyListItem[]> {
    assertTenantClaim(
      this.scope,
      { organizationId: this.scope.organizationId, projectId },
      "ApiKeyService.listForProject"
    );
    const result = await this.db
      .prepare(
        `SELECT ak.id, ak.name, ak.description, ak.key_prefix, ak.role, p.environment, ak.status,
                CASE
                  WHEN EXISTS (
                    SELECT 1
                    FROM api_key_wallet_permissions akw
                    WHERE akw.api_key_id = ak.id
                  )
                  THEN 'selected'
                  ELSE 'all'
                END AS wallet_scope,
                ak.signing_wallet_id, ak.last_used_at, ak.expires_at, ak.created_at
         FROM api_keys ak
         JOIN projects p ON p.id = ak.project_id
         WHERE ak.organization_id = ?
           AND ak.project_id = ?
           AND ak.status NOT IN ('revoked', 'deactivated')
         ORDER BY ak.created_at DESC`
      )
      .bind(this.scope.organizationId, this.scope.projectId)
      .all<ApiKeyListRow>();

    return result.results.map((row) => this.mapListRow(row));
  }

  async getDetails(
    keyId: string,
    organizationId: string,
    projectId: string
  ): Promise<ApiKeyDetails | null> {
    assertTenantClaim(this.scope, { organizationId, projectId }, "ApiKeyService.getDetails");
    const row = await this.db
      .prepare(
        `SELECT ak.id, ak.name, ak.description, ak.key_prefix, ak.role, p.environment, ak.status,
                ak.project_id, ak.allowed_ips, ak.permissions, ak.signing_wallet_id,
                CASE
                  WHEN EXISTS (
                    SELECT 1
                    FROM api_key_wallet_permissions akw
                    WHERE akw.api_key_id = ak.id
                  )
                  THEN 'selected'
                  ELSE 'all'
                END AS wallet_scope,
                ak.last_used_at, ak.expires_at, ak.rotated_from, ak.rotation_deadline, ak.created_at
         FROM api_keys ak
         JOIN projects p ON p.id = ak.project_id
         WHERE ak.id = ? AND ak.organization_id = ? AND ak.project_id = ?`
      )
      .bind(keyId, organizationId, projectId)
      .first<ApiKeyDetailsRow>();

    if (!row) {
      return null;
    }

    return {
      ...this.mapListRow(row),
      projectId: row.project_id,
      allowedIps: parseOptionalPostgresJson<string[]>(row.allowed_ips),
      permissions: row.permissions ? parsePostgresJson<Permission[]>(row.permissions) : null,
      signingWalletId: row.signing_wallet_id,
      rotatedFrom: row.rotated_from,
      rotationDeadline: row.rotation_deadline,
    };
  }

  async createApiKey(input: CreateApiKeyInput): Promise<CreateApiKeyResult> {
    assertTenantClaim(this.scope, input, "ApiKeyService.createApiKey");
    assertGrantableApiKeyPermissions(input.actorPermissions, input.role, input.permissions);

    const project = await this.db
      .prepare(`SELECT environment FROM projects WHERE id = ? AND organization_id = ?`)
      .bind(input.projectId, input.organizationId)
      .first<{ environment: ApiKeyEnvironment }>();

    if (!project) {
      throw new AppError("NOT_FOUND", "Project not found");
    }

    const keyId = `key_${crypto.randomUUID()}`;
    const { key, prefix } = createApiKeyMaterial(project.environment);
    const keyHash = await hashString(key, input.pepper);

    let createdBy = input.createdByUserId?.trim() || "";

    if (!createdBy && input.createdByKeyId) {
      const creatorKey = await this.db
        .prepare(
          `SELECT created_by
           FROM api_keys
           WHERE id = ? AND organization_id = ? AND project_id = ?`
        )
        .bind(input.createdByKeyId, this.scope.organizationId, this.scope.projectId)
        .first<{ created_by: string }>();
      createdBy = creatorKey?.created_by || "";
    }

    const actor = await this.db
      .prepare("SELECT id FROM users WHERE id = ?")
      .bind(createdBy)
      .first<{ id: string }>();

    if (!actor) {
      throw new AppError("INTERNAL_ERROR", "Authenticated actor could not be resolved");
    }

    try {
      await this.db
        .prepare(
          `INSERT INTO api_keys (
            id, organization_id, project_id, created_by, name, description, key_prefix, key_hash,
            role, permissions, allowed_ips, signing_wallet_id, expires_at, status
          ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'active')`
        )
        .bind(
          keyId,
          input.organizationId,
          input.projectId,
          createdBy,
          input.name,
          input.description ?? null,
          prefix,
          keyHash,
          input.role,
          input.permissions ? JSON.stringify(input.permissions) : null,
          input.allowedIps ? JSON.stringify(input.allowedIps) : null,
          input.signingWalletId ?? null,
          input.expiresAt ?? null
        )
        .run();
    } catch (error) {
      if (error instanceof Error && error.message.includes("UNIQUE")) {
        throw new AppError(
          "CONFLICT",
          "Failed to create API key due to a key collision. Please retry."
        );
      }

      if (error instanceof Error && error.message.includes("FOREIGN KEY")) {
        throw new AppError(
          "INTERNAL_ERROR",
          "Creator account is not linked to this organization. Re-authenticate and try again."
        );
      }

      throw error;
    }

    return {
      id: keyId,
      name: input.name,
      key,
      keyPrefix: prefix,
      role: input.role,
      environment: project.environment,
      expiresAt: input.expiresAt ?? null,
      createdAt: new Date().toISOString(),
      keyHash,
    };
  }

  async updateApiKey(input: UpdateApiKeyInput): Promise<void> {
    assertTenantClaim(this.scope, input, "ApiKeyService.updateApiKey");
    if (input.permissions !== undefined) {
      assertGrantableApiKeyPermissions(
        input.actorPermissions,
        input.currentRole,
        input.permissions
      );
    }

    const updates: string[] = [];
    const values: (string | null)[] = [];

    if (input.name !== undefined) {
      updates.push("name = ?");
      values.push(input.name);
    }
    if (input.description !== undefined) {
      updates.push("description = ?");
      values.push(input.description);
    }
    if (input.allowedIps !== undefined) {
      updates.push("allowed_ips = ?");
      values.push(input.allowedIps ? JSON.stringify(input.allowedIps) : null);
    }
    if (input.expiresAt !== undefined) {
      updates.push("expires_at = ?");
      values.push(input.expiresAt);
    }
    if (input.permissions !== undefined) {
      updates.push("permissions = ?");
      values.push(input.permissions ? JSON.stringify(input.permissions) : null);
    }
    if (input.signingWallet) {
      updates.push("signing_wallet_id = ?");
      values.push(input.signingWallet.walletId);
    }

    if (updates.length === 0) {
      throw badRequest("No fields to update");
    }

    values.push(input.keyId, input.organizationId, input.projectId);
    await this.db
      .prepare(
        `UPDATE api_keys SET ${updates.join(", ")} WHERE id = ? AND organization_id = ? AND project_id = ?`
      )
      .bind(...values)
      .run();
  }

  async rotateApiKey(
    keyId: string,
    organizationId: string,
    projectId: string,
    gracePeriodHours: number,
    pepper?: string
  ): Promise<RotateApiKeyResult | null> {
    assertTenantClaim(this.scope, { organizationId, projectId }, "ApiKeyService.rotateApiKey");
    const existing = await this.db
      .prepare(
        `SELECT ak.id, ak.name, ak.description, ak.key_hash, ak.role, ak.permissions,
                p.environment, ak.project_id, ak.allowed_ips, ak.signing_wallet_id, ak.created_by
         FROM api_keys ak
         JOIN projects p ON p.id = ak.project_id
         WHERE ak.id = ? AND ak.organization_id = ? AND ak.project_id = ? AND ak.status = 'active'`
      )
      .bind(keyId, organizationId, projectId)
      .first<{
        id: string;
        name: string;
        description: string | null;
        key_hash: string;
        role: ApiKeyRole;
        permissions: string | null;
        environment: ApiKeyEnvironment;
        project_id: string;
        allowed_ips: string | null;
        signing_wallet_id: string | null;
        created_by: string;
      }>();

    if (!existing) {
      return null;
    }

    const newKeyId = `key_${crypto.randomUUID()}`;
    const { key: newKey, prefix: newPrefix } = createApiKeyMaterial(existing.environment);
    const newKeyHash = await hashString(newKey, pepper);

    const rotationDeadline = new Date(Date.now() + gracePeriodHours * 60 * 60 * 1000).toISOString();

    await this.db.transaction(async (tx) => {
      await tx
        .prepare(
          `INSERT INTO api_keys (
            id, organization_id, project_id, created_by, name, description, key_prefix, key_hash,
            role, permissions, allowed_ips, signing_wallet_id, rotated_from, status
          ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'active')`
        )
        .bind(
          newKeyId,
          organizationId,
          existing.project_id,
          existing.created_by,
          existing.name,
          existing.description,
          newPrefix,
          newKeyHash,
          existing.role,
          existing.permissions,
          existing.allowed_ips,
          existing.signing_wallet_id,
          keyId
        )
        .run();

      await tx
        .prepare(
          `UPDATE api_keys
           SET rotation_deadline = ?
           WHERE id = ? AND organization_id = ? AND project_id = ?`
        )
        .bind(rotationDeadline, keyId, this.scope.organizationId, this.scope.projectId)
        .run();

      await tx
        .prepare(
          `INSERT INTO api_key_wallet_permissions (id, api_key_id, wallet_id, permissions)
           SELECT
             'akw_' || md5(random()::text || clock_timestamp()::text),
             ?,
             wallet_id,
             permissions
           FROM api_key_wallet_permissions
           WHERE api_key_id = ?`
        )
        .bind(newKeyId, keyId)
        .run();

      await this.cloneApiKeyPolicyFoundation(tx, keyId, newKeyId);
    });

    return {
      apiKey: {
        id: newKeyId,
        name: existing.name,
        key: newKey,
        keyPrefix: newPrefix,
        role: existing.role,
        environment: existing.environment,
        expiresAt: null,
        createdAt: new Date().toISOString(),
      },
      previousKey: {
        id: keyId,
        rotationDeadline,
      },
      previousKeyHash: existing.key_hash,
    };
  }

  async revokeApiKey(
    keyId: string,
    organizationId: string,
    projectId: string
  ): Promise<{
    keyHash: string;
    revokedAt: string;
  } | null> {
    assertTenantClaim(this.scope, { organizationId, projectId }, "ApiKeyService.revokeApiKey");
    const key = await this.db
      .prepare(
        "SELECT id, key_hash FROM api_keys WHERE id = ? AND organization_id = ? AND project_id = ?"
      )
      .bind(keyId, organizationId, projectId)
      .first<{ id: string; key_hash: string }>();

    if (!key) {
      return null;
    }

    const now = new Date().toISOString();
    await this.db
      .prepare(
        `UPDATE api_keys
         SET status = 'deactivated', revoked_at = ?
         WHERE id = ? AND organization_id = ? AND project_id = ?`
      )
      .bind(now, keyId, this.scope.organizationId, this.scope.projectId)
      .run();

    return { keyHash: key.key_hash, revokedAt: now };
  }

  private mapListRow(row: ApiKeyListRow): ApiKeyListItem {
    return {
      id: row.id,
      name: row.name,
      description: row.description,
      keyPrefix: row.key_prefix,
      role: row.role,
      environment: row.environment,
      status: row.status,
      walletScope: row.wallet_scope,
      signingWalletId: row.signing_wallet_id,
      lastUsedAt: row.last_used_at,
      expiresAt: row.expires_at,
      createdAt: row.created_at,
    };
  }

  private async cloneApiKeyPolicyFoundation(
    db: DatabaseExecutor,
    sourceApiKeyId: string,
    targetApiKeyId: string
  ): Promise<void> {
    const profileRows = await db
      .prepare(
        `SELECT *
         FROM api_key_control_profiles
         WHERE api_key_id = ?
         ORDER BY created_at ASC`
      )
      .bind(sourceApiKeyId)
      .all<Record<string, unknown>>();

    const profileIdMap = new Map<string, string>();
    const revisionIdMap = new Map<string, string>();

    for (const profile of profileRows.results) {
      const sourceProfileId = profile.id as string;
      const targetProfileId = `akcp_${crypto.randomUUID()}`;
      profileIdMap.set(sourceProfileId, targetProfileId);

      await db
        .prepare(
          `INSERT INTO api_key_control_profiles (
             id,
             organization_id,
             project_id,
             api_key_id,
             name,
             status,
             created_by,
             activated_at,
             archived_at
           ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
        )
        .bind(
          targetProfileId,
          profile.organization_id,
          profile.project_id,
          targetApiKeyId,
          profile.name,
          profile.status,
          profile.created_by ?? null,
          profile.activated_at ?? null,
          profile.archived_at ?? null
        )
        .run();

      const revisionRows = await db
        .prepare(
          `SELECT *
           FROM api_key_control_profile_revisions
           WHERE profile_id = ?
           ORDER BY revision_number ASC`
        )
        .bind(sourceProfileId)
        .all<Record<string, unknown>>();

      for (const revision of revisionRows.results) {
        const sourceRevisionId = revision.id as string;
        const targetRevisionId = `akcpr_${crypto.randomUUID()}`;
        revisionIdMap.set(sourceRevisionId, targetRevisionId);

        await db
          .prepare(
            `INSERT INTO api_key_control_profile_revisions (
               id,
               profile_id,
               revision_number,
               rules,
               default_action,
               created_by,
               activated_at
             ) VALUES (?, ?, ?, ?::jsonb, ?, ?, ?)`
          )
          .bind(
            targetRevisionId,
            targetProfileId,
            revision.revision_number,
            stringifyJsonb(revision.rules, []),
            revision.default_action,
            revision.created_by ?? null,
            revision.activated_at ?? null
          )
          .run();
      }

      await db
        .prepare(
          `UPDATE api_key_control_profiles
           SET active_revision_id = ?,
               updated_at = sdp_iso_now()
           WHERE id = ?`
        )
        .bind(
          profile.active_revision_id
            ? (revisionIdMap.get(profile.active_revision_id as string) ?? null)
            : null,
          targetProfileId
        )
        .run();
    }

    const bindingRows = await db
      .prepare(
        `SELECT *
         FROM api_key_wallet_policy_bindings
         WHERE api_key_id = ?
         ORDER BY created_at ASC`
      )
      .bind(sourceApiKeyId)
      .all<ApiKeyWalletPolicyBindingRow>();

    for (const binding of bindingRows.results) {
      let apiKeyControlProfileId: string | null = null;
      if (binding.api_key_control_profile_id !== null) {
        const mappedProfileId = profileIdMap.get(binding.api_key_control_profile_id);
        if (mappedProfileId === undefined) {
          throw internalError("Failed to clone API key policy binding profile");
        }
        apiKeyControlProfileId = mappedProfileId;
      }

      await db
        .prepare(
          `INSERT INTO api_key_wallet_policy_bindings (
             id,
             api_key_id,
             binding_scope,
             wallet_id,
             custody_wallet_id,
             wallet_control_profile_id,
             api_key_control_profile_id
           ) VALUES (?, ?, ?, ?, ?, ?, ?)`
        )
        .bind(
          `akwpol_${crypto.randomUUID()}`,
          targetApiKeyId,
          binding.binding_scope,
          binding.wallet_id,
          binding.custody_wallet_id,
          binding.wallet_control_profile_id,
          apiKeyControlProfileId
        )
        .run();
    }
  }
}
