/**
 * Per-workspace GCS provisioning (P5.3).
 *
 * Idempotently sets up everything a workspace needs to do object storage:
 *   1. A dedicated GCP service account (vibn-ws-{slug}@…)
 *   2. A JSON keyfile for that SA (encrypted at rest)
 *   3. A default GCS bucket (vibn-ws-{slug}-{6char}) in northamerica-northeast1
 *   4. A bucket-scoped roles/storage.objectAdmin binding for the SA
 *   5. An HMAC key on the SA so app code can use AWS S3 SDKs
 *
 * Persists IDs + encrypted secrets onto vibn_workspaces. Safe to re-run;
 * each step is idempotent and short-circuits when already complete.
 *
 * Required schema migration: scripts/migrate-workspace-gcs.sql
 *
 * The control plane itself never decrypts the per-workspace SA key — it
 * always authenticates as the shared `vibn-workspace-provisioner`. The
 * per-workspace credentials exist solely to be injected into deployed
 * Coolify apps as STORAGE_* env vars (see app env injection in
 * apps/route.ts when wired up).
 */

import { query, queryOne } from '@/lib/db-postgres';
import { encryptSecret, decryptSecret } from '@/lib/auth/secret-box';
import {
  ensureWorkspaceServiceAccount,
  workspaceServiceAccountEmail,
  createServiceAccountKey,
} from '@/lib/gcp/iam';
import {
  createBucket,
  getBucket,
  addBucketIamBinding,
  createHmacKey,
  listHmacKeysForServiceAccount,
  workspaceDefaultBucketName,
  VIBN_GCS_LOCATION,
  type GcsBucket,
} from '@/lib/gcp/storage';
import type { VibnWorkspace } from '@/lib/workspaces';

/**
 * Extra columns added by scripts/migrate-workspace-gcs.sql. We model
 * them as a separate interface so the existing `VibnWorkspace` shape
 * doesn't have to be touched until every caller is ready.
 */
export interface VibnWorkspaceGcs {
  gcp_service_account_email: string | null;
  gcp_service_account_key_enc: string | null;
  gcs_default_bucket_name: string | null;
  gcs_hmac_access_id: string | null;
  gcs_hmac_secret_enc: string | null;
  gcp_provision_status: 'pending' | 'partial' | 'ready' | 'error';
  gcp_provision_error: string | null;
}

export type WorkspaceGcs = VibnWorkspace & VibnWorkspaceGcs;

export async function getWorkspaceGcsState(workspaceId: string): Promise<WorkspaceGcs | null> {
  return queryOne<WorkspaceGcs>(
    `SELECT * FROM vibn_workspaces WHERE id = $1`,
    [workspaceId],
  );
}

/** What we tell the API caller after a successful provision. */
export interface WorkspaceGcsResult {
  serviceAccountEmail: string;
  bucket: {
    name: string;
    location: string;
    selfLink?: string;
    timeCreated?: string;
  };
  hmac: {
    accessId: string;
  };
  status: VibnWorkspaceGcs['gcp_provision_status'];
}

/**
 * Idempotent: ensures the workspace has a GCP SA + key + default bucket
 * + IAM binding + HMAC key. Updates vibn_workspaces with the resulting
 * identifiers (key + secret stored encrypted). Returns a flat summary
 * suitable for sending back to the API caller.
 *
 * Throws on any irrecoverable error; transient/partial failures land in
 * the row's gcp_provision_status='partial' with the message in
 * gcp_provision_error.
 */
export async function ensureWorkspaceGcsProvisioned(
  workspace: VibnWorkspace,
): Promise<WorkspaceGcsResult> {
  const ws = (await getWorkspaceGcsState(workspace.id)) ?? (workspace as WorkspaceGcs);

  // ── Short-circuit if everything is already there.
  if (
    ws.gcp_provision_status === 'ready' &&
    ws.gcp_service_account_email &&
    ws.gcs_default_bucket_name &&
    ws.gcs_hmac_access_id
  ) {
    const existing = await getBucket(ws.gcs_default_bucket_name);
    if (existing) {
      return {
        serviceAccountEmail: ws.gcp_service_account_email,
        bucket: {
          name: existing.name,
          location: existing.location,
          selfLink: existing.selfLink,
          timeCreated: existing.timeCreated,
        },
        hmac: { accessId: ws.gcs_hmac_access_id },
        status: 'ready',
      };
    }
    // Bucket vanished out from under us (manual gcloud delete?). Fall
    // through and re-provision; the SA + HMAC can stay.
  }

  let saEmail = ws.gcp_service_account_email;
  let saKeyEnc = ws.gcp_service_account_key_enc;
  let bucketName = ws.gcs_default_bucket_name;
  let hmacAccessId = ws.gcs_hmac_access_id;
  let hmacSecretEnc = ws.gcs_hmac_secret_enc;
  let bucket: GcsBucket | null = null;
  const errors: string[] = [];

  // ── 1. Service account ─────────────────────────────────────────────
  try {
    const sa = await ensureWorkspaceServiceAccount({
      slug: workspace.slug,
      workspaceName: workspace.name,
    });
    saEmail = sa.email;
  } catch (err) {
    const msg = err instanceof Error ? err.message : String(err);
    errors.push(`gcp-sa: ${msg}`);
    saEmail = saEmail ?? workspaceServiceAccountEmail(workspace.slug);
  }

  // ── 2. SA keyfile ─────────────────────────────────────────────────
  // Mint once. Rotation is a separate flow (Tier 2 territory).
  if (!saKeyEnc && saEmail && !errors.some(e => e.startsWith('gcp-sa:'))) {
    try {
      const key = await createServiceAccountKey(saEmail);
      // privateKeyData is already base64; we encrypt the whole base64
      // payload so the column can stay TEXT and reuse secret-box.
      saKeyEnc = encryptSecret(key.privateKeyData);
    } catch (err) {
      const msg = err instanceof Error ? err.message : String(err);
      errors.push(`gcp-sa-key: ${msg}`);
    }
  }

  // ── 3. Default bucket ──────────────────────────────────────────────
  if (!bucketName) bucketName = workspaceDefaultBucketName(workspace.slug);
  if (!errors.some(e => e.startsWith('gcp-sa:'))) {
    try {
      bucket =
        (await getBucket(bucketName)) ??
        (await createBucket({
          name: bucketName,
          location: VIBN_GCS_LOCATION,
          enforcePublicAccessPrevention: true,
          workspaceSlug: workspace.slug,
        }));
    } catch (err) {
      const msg = err instanceof Error ? err.message : String(err);
      errors.push(`gcs-bucket: ${msg}`);
    }
  }

  // ── 4. Bucket IAM binding for the workspace SA ─────────────────────
  if (bucket && saEmail) {
    try {
      await addBucketIamBinding({
        bucketName: bucket.name,
        role: 'roles/storage.objectAdmin',
        member: `serviceAccount:${saEmail}`,
      });
    } catch (err) {
      const msg = err instanceof Error ? err.message : String(err);
      errors.push(`gcs-iam: ${msg}`);
    }
  }

  // ── 5. HMAC key for app code ───────────────────────────────────────
  // Only mint if we don't already have one. GCS limits 5 active keys
  // per SA; we never want to thrash this.
  if (saEmail && !hmacAccessId) {
    try {
      // Defensive: if a previous run minted a key but failed before
      // saving, reuse the existing ACTIVE one instead of stacking.
      const existingHmacs = await listHmacKeysForServiceAccount(saEmail);
      const active = existingHmacs.find(k => k.state === 'ACTIVE');
      if (active) {
        hmacAccessId = active.accessId;
        // We can't recover the secret of a previously-minted key; leave
        // the encrypted secret null and let the operator rotate if they
        // need it injected.
      } else {
        const minted = await createHmacKey(saEmail);
        hmacAccessId = minted.accessId;
        hmacSecretEnc = encryptSecret(minted.secret);
      }
    } catch (err) {
      const msg = err instanceof Error ? err.message : String(err);
      errors.push(`gcs-hmac: ${msg}`);
    }
  }

  const allReady = !!(saEmail && saKeyEnc && bucket && hmacAccessId && errors.length === 0);
  const status: VibnWorkspaceGcs['gcp_provision_status'] = allReady
    ? 'ready'
    : errors.length > 0
      ? 'partial'
      : 'pending';

  await query(
    `UPDATE vibn_workspaces
     SET gcp_service_account_email   = COALESCE($2, gcp_service_account_email),
         gcp_service_account_key_enc = COALESCE($3, gcp_service_account_key_enc),
         gcs_default_bucket_name     = COALESCE($4, gcs_default_bucket_name),
         gcs_hmac_access_id          = COALESCE($5, gcs_hmac_access_id),
         gcs_hmac_secret_enc         = COALESCE($6, gcs_hmac_secret_enc),
         gcp_provision_status        = $7,
         gcp_provision_error         = $8,
         updated_at                  = now()
     WHERE id = $1`,
    [
      workspace.id,
      saEmail,
      saKeyEnc,
      bucket?.name ?? bucketName,
      hmacAccessId,
      hmacSecretEnc,
      status,
      errors.length ? errors.join('; ') : null,
    ],
  );

  if (!saEmail) throw new Error(`workspace-gcs: SA email never resolved: ${errors.join('; ')}`);
  if (!bucket) throw new Error(`workspace-gcs: bucket never created: ${errors.join('; ')}`);

  return {
    serviceAccountEmail: saEmail,
    bucket: {
      name: bucket.name,
      location: bucket.location,
      selfLink: bucket.selfLink,
      timeCreated: bucket.timeCreated,
    },
    hmac: { accessId: hmacAccessId ?? '' },
    status,
  };
}

/**
 * Decrypt the workspace's HMAC secret for STORAGE_SECRET_ACCESS_KEY env
 * injection. Returns null when not provisioned or decrypt fails.
 *
 * Callers MUST treat this as shown-once material: log neither the
 * value nor anything that contains it.
 */
export function getWorkspaceGcsHmacCredentials(ws: WorkspaceGcs): {
  accessId: string;
  secret: string;
} | null {
  if (!ws.gcs_hmac_access_id || !ws.gcs_hmac_secret_enc) return null;
  try {
    return {
      accessId: ws.gcs_hmac_access_id,
      secret: decryptSecret(ws.gcs_hmac_secret_enc),
    };
  } catch (err) {
    console.error('[workspace-gcs] failed to decrypt HMAC secret for', ws.slug, err);
    return null;
  }
}
