/**
 * Vibn workspaces — logical multi-tenancy on top of Coolify + Gitea.
 *
 * Each Vibn user gets one workspace. The workspace owns:
 *   - a Coolify Project UUID (the team/namespace boundary inside Coolify)
 *   - a Gitea org (which contains all repos for the workspace)
 *
 * All Vibn projects, apps, deployments, and AI access keys are
 * scoped to a single workspace. Code that touches Coolify or Gitea
 * MUST resolve a workspace first and use its IDs (never the legacy
 * hardcoded admin user / project UUID).
 *
 * Coolify cannot create real Teams via its public API today — see
 * Coolify changelog notes about scoping queries to current team and
 * the lack of POST /teams. We treat one Coolify *Project* as our
 * tenant boundary instead, and stamp `coolify_team_id` later if/when
 * Coolify exposes team creation.
 */

import { randomBytes } from 'crypto';
import { query, queryOne } from '@/lib/db-postgres';
import {
  createProject as createCoolifyProject,
  createPrivateKey as createCoolifyPrivateKey,
  COOLIFY_DEFAULT_SERVER_UUID,
  COOLIFY_DEFAULT_DESTINATION_UUID,
} from '@/lib/coolify';
import { mintWorkspaceApiKey } from '@/lib/auth/workspace-auth';
import {
  createOrg,
  getOrg,
  getUser,
  addOrgOwner,
  createUser,
  createAccessTokenFor,
  ensureOrgTeamMembership,
  adminEditUser,
  adminAddUserSshKey,
  adminListUserSshKeys,
} from '@/lib/gitea';
import { encryptSecret, decryptSecret } from '@/lib/auth/secret-box';
import { generateEd25519Keypair } from '@/lib/ssh-keys';

export interface VibnWorkspace {
  id: string;
  slug: string;
  name: string;
  owner_user_id: string;
  coolify_project_uuid: string | null;
  coolify_team_id: number | null;
  coolify_server_uuid: string | null;
  coolify_destination_uuid: string | null;
  coolify_environment_name: string;
  coolify_private_key_uuid: string | null;
  gitea_org: string | null;
  gitea_bot_username: string | null;
  gitea_bot_user_id: number | null;
  gitea_bot_token_encrypted: string | null;
  gitea_bot_ssh_key_id: number | null;
  provision_status: 'pending' | 'partial' | 'ready' | 'error';
  provision_error: string | null;
  created_at: Date;
  updated_at: Date;
}

export interface VibnWorkspaceMember {
  id: string;
  workspace_id: string;
  user_id: string;
  role: 'owner' | 'admin' | 'member';
  created_at: Date;
}

// ──────────────────────────────────────────────────
// Slug helpers
// ──────────────────────────────────────────────────

const SAFE_SLUG = /[^a-z0-9]+/g;

export function workspaceSlugFromEmail(email: string): string {
  const local = email.split('@')[0]?.toLowerCase() ?? 'user';
  return local.replace(SAFE_SLUG, '-').replace(/^-+|-+$/g, '') || 'user';
}

/** Coolify Project name we use for a workspace. Prefixed to avoid collisions. */
export function coolifyProjectNameFor(slug: string): string {
  return `vibn-ws-${slug}`;
}

/** Gitea org name we use for a workspace. Same prefix for consistency. */
export function giteaOrgNameFor(slug: string): string {
  return `vibn-${slug}`;
}

/** Gitea username we mint for each workspace's bot account. */
export function giteaBotUsernameFor(slug: string): string {
  return `vibn-bot-${slug}`;
}

/** Placeholder-looking email so Gitea accepts the user; never delivered to. */
export function giteaBotEmailFor(slug: string): string {
  return `bot+${slug}@vibnai.invalid`;
}

// ──────────────────────────────────────────────────
// CRUD
// ──────────────────────────────────────────────────

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

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

export async function getWorkspaceByOwner(userId: string): Promise<VibnWorkspace | null> {
  return queryOne<VibnWorkspace>(
    `SELECT * FROM vibn_workspaces WHERE owner_user_id = $1 ORDER BY created_at ASC LIMIT 1`,
    [userId]
  );
}

export async function listWorkspacesForUser(userId: string): Promise<VibnWorkspace[]> {
  return query<VibnWorkspace>(
    `SELECT w.* FROM vibn_workspaces w
     LEFT JOIN vibn_workspace_members m ON m.workspace_id = w.id
     WHERE w.owner_user_id = $1 OR m.user_id = $1
     GROUP BY w.id
     ORDER BY w.created_at ASC`,
    [userId]
  );
}

export async function userHasWorkspaceAccess(userId: string, workspaceId: string): Promise<boolean> {
  const row = await queryOne<{ id: string }>(
    `SELECT w.id FROM vibn_workspaces w
     LEFT JOIN vibn_workspace_members m ON m.workspace_id = w.id AND m.user_id = $1
     WHERE w.id = $2 AND (w.owner_user_id = $1 OR m.user_id = $1)
     LIMIT 1`,
    [userId, workspaceId]
  );
  return !!row;
}

// ──────────────────────────────────────────────────
// Get-or-create + provision
// ──────────────────────────────────────────────────

/**
 * Idempotently ensures a workspace row exists for the user. Does NOT
 * provision Coolify/Gitea — call ensureWorkspaceProvisioned() for that.
 *
 * Suitable to call from the NextAuth signIn callback (cheap, single insert).
 */
export async function ensureWorkspaceForUser(opts: {
  userId: string;
  email: string;
  displayName?: string | null;
}): Promise<VibnWorkspace> {
  const existing = await getWorkspaceByOwner(opts.userId);
  if (existing) return existing;

  const slug = await pickAvailableSlug(workspaceSlugFromEmail(opts.email));
  const name = opts.displayName?.trim() || opts.email.split('@')[0];

  const inserted = await query<VibnWorkspace>(
    `INSERT INTO vibn_workspaces (slug, name, owner_user_id)
     VALUES ($1, $2, $3)
     RETURNING *`,
    [slug, name, opts.userId]
  );
  const workspace = inserted[0];

  await query(
    `INSERT INTO vibn_workspace_members (workspace_id, user_id, role)
     VALUES ($1, $2, 'owner')
     ON CONFLICT (workspace_id, user_id) DO NOTHING`,
    [workspace.id, opts.userId]
  );

  // Auto-mint a default API key so the chat panel and Cursor MCP
  // integration work without any manual setup step.
  try {
    await mintWorkspaceApiKey({
      workspaceId: workspace.id,
      name: 'default',
      createdBy: opts.userId,
      scopes: ['workspace:*'],
    });
  } catch (err) {
    // Non-fatal — user can mint manually in Settings if this fails.
    console.error('[workspaces] auto-mint default key failed', workspace.slug, err);
  }

  return workspace;
}

/**
 * Provisions Coolify Project + Gitea org for a workspace if not yet done.
 * Idempotent. Failures are recorded on the row but do not throw — callers
 * can retry by calling again. Returns the up-to-date workspace row.
 */
export async function ensureWorkspaceProvisioned(workspace: VibnWorkspace): Promise<VibnWorkspace> {
  // Only short-circuit when every sub-resource is actually present.
  // We've seen "ready but missing bot token" cases (e.g. after an
  // operational rotation that nulled the encrypted token) and the
  // status check alone would silently skip re-minting.
  const fullyProvisioned =
    workspace.provision_status === 'ready' &&
    workspace.coolify_project_uuid &&
    workspace.gitea_org &&
    workspace.gitea_bot_username &&
    workspace.gitea_bot_token_encrypted &&
    workspace.coolify_private_key_uuid &&
    workspace.gitea_bot_ssh_key_id;
  if (fullyProvisioned) return workspace;

  let coolifyUuid = workspace.coolify_project_uuid;
  let giteaOrg = workspace.gitea_org;
  let coolifyServerUuid = workspace.coolify_server_uuid ?? COOLIFY_DEFAULT_SERVER_UUID;
  let coolifyDestinationUuid =
    workspace.coolify_destination_uuid ?? COOLIFY_DEFAULT_DESTINATION_UUID;
  let coolifyPrivateKeyUuid = workspace.coolify_private_key_uuid;
  let giteaBotSshKeyId = workspace.gitea_bot_ssh_key_id;
  const errors: string[] = [];

  // ── Coolify Project ────────────────────────────────────────────────
  if (!coolifyUuid) {
    try {
      const project = await createCoolifyProject(
        coolifyProjectNameFor(workspace.slug),
        `Vibn workspace ${workspace.slug}`
      );
      coolifyUuid = project.uuid;
    } catch (err) {
      const msg = err instanceof Error ? err.message : String(err);
      // Coolify returns 400/409 if the name collides — fall through; the
      // workspace can still be patched manually with the right UUID.
      errors.push(`coolify: ${msg}`);
      console.error('[workspaces] Coolify provisioning failed', workspace.slug, msg);
    }
  }

  // ── Gitea Org ──────────────────────────────────────────────────────
  if (!giteaOrg) {
    const wantOrg = giteaOrgNameFor(workspace.slug);
    try {
      const existingOrg = await getOrg(wantOrg);
      if (existingOrg) {
        giteaOrg = existingOrg.username;
      } else {
        const created = await createOrg({
          name: wantOrg,
          fullName: workspace.name,
          description: `Vibn workspace for ${workspace.slug}`,
          visibility: 'private',
        });
        giteaOrg = created.username;
      }
    } catch (err) {
      const msg = err instanceof Error ? err.message : String(err);
      errors.push(`gitea: ${msg}`);
      console.error('[workspaces] Gitea org provisioning failed', workspace.slug, msg);
    }
  }

  // ── Add the workspace owner to the Gitea org if they have a Gitea account.
  // Best-effort: most Vibn users won't have a Gitea login, so a 404 is fine.
  if (giteaOrg) {
    try {
      const ownerEmail = await queryOne<{ email: string }>(
        `SELECT data->>'email' AS email FROM fs_users WHERE id = $1`,
        [workspace.owner_user_id]
      );
      const candidateLogin = ownerEmail?.email
        ? workspaceSlugFromEmail(ownerEmail.email)
        : null;
      if (candidateLogin) {
        const giteaUser = await getUser(candidateLogin);
        if (giteaUser) {
          await addOrgOwner(giteaOrg, giteaUser.login);
        }
      }
    } catch (err) {
      // Membership add is best-effort
      console.warn('[workspaces] Skipping Gitea owner add', err);
    }
  }

  // ── Per-workspace Gitea bot user + PAT ─────────────────────────────
  // Gives AI agents a credential that is scoped exclusively to this
  // workspace's org. The bot has no other org memberships, so even a
  // leaked PAT cannot reach other tenants' repos.
  let botUsername = workspace.gitea_bot_username;
  let botUserId = workspace.gitea_bot_user_id;
  let botTokenEncrypted = workspace.gitea_bot_token_encrypted;

  if (giteaOrg && (!botUsername || !botTokenEncrypted)) {
    try {
      const wantBot = giteaBotUsernameFor(workspace.slug);

      // 1. Ensure the bot user exists. We *always* generate a fresh
      //    random password — if the user already exists, we can't
      //    retrieve its password to auth with, so we'll fail at the
      //    token-mint step. The `partial` provision status + error
      //    message tells the operator to delete the bot user in Gitea
      //    (or reset its password) and re-run. Since this only hurts
      //    re-provisioning after a half-failed run, we also try to
      //    reset the password via the admin API in that case.
      const password = `bot-${randomBytes(24).toString('base64url')}`;

      let existingBot = await getUser(wantBot);
      if (!existingBot) {
        const created = await createUser({
          username: wantBot,
          email: giteaBotEmailFor(workspace.slug),
          password,
          fullName: `Vibn bot (${workspace.slug})`,
        });
        existingBot = { id: created.id, login: created.login };
      } else {
        // Existing bot from a half-failed previous run — reset its
        // password so we can basic-auth as it below.
        await adminEditUser({ username: wantBot, password });
      }
      botUsername = existingBot.login;
      botUserId = existingBot.id;

      // 2. Add the bot to the org's Writers team (scoped permissions).
      await ensureOrgTeamMembership({
        org: giteaOrg,
        teamName: 'Writers',
        permission: 'write',
        username: botUsername,
      });

      // 3. Mint a PAT for the bot. Gitea shows the plaintext exactly
      //    once, so if we already stored an encrypted copy we skip.
      if (!botTokenEncrypted) {
        const pat = await createAccessTokenFor({
          username: botUsername,
          password,
          name: `vibn-${workspace.slug}-${Date.now().toString(36)}`,
          scopes: [
            'write:repository',
            'write:issue',
            'write:user',
            'write:organization',
          ],
        });
        botTokenEncrypted = encryptSecret(pat.sha1);
      }
    } catch (err) {
      const msg = err instanceof Error ? err.message : String(err);
      errors.push(`gitea-bot: ${msg}`);
      console.error('[workspaces] Gitea bot provisioning failed', workspace.slug, msg);
    }
  }

  // ── Per-workspace SSH deploy keypair ──────────────────────────────
  // Used by Coolify to clone private Gitea repos this workspace owns.
  // We generate the keypair in-process, push the public key to Gitea
  // (under the bot user so its team memberships scope repo access),
  // and register the private key in Coolify. Neither key is ever
  // persisted directly by Vibn — we only keep their ids.
  if (botUsername && (!coolifyPrivateKeyUuid || !giteaBotSshKeyId)) {
    try {
      const comment = `vibn-${workspace.slug}@${botUsername}`;
      const kp = generateEd25519Keypair(comment);

      // Register public key on Gitea bot if not already present.
      if (!giteaBotSshKeyId) {
        // Protect against double-adding on re-provisioning by looking
        // for an existing key with our canonical title first.
        const title = `vibn-${workspace.slug}-coolify`;
        let existingId: number | null = null;
        try {
          const keys = await adminListUserSshKeys(botUsername);
          existingId = keys.find(k => k.title === title)?.id ?? null;
        } catch {
          /* list failure is non-fatal */
        }
        if (existingId) {
          giteaBotSshKeyId = existingId;
        } else {
          const added = await adminAddUserSshKey({
            username: botUsername,
            title,
            key: kp.publicKeyOpenSsh,
            readOnly: false,
          });
          giteaBotSshKeyId = added.id;
        }
      }

      // Register private key in Coolify if not already present.
      if (!coolifyPrivateKeyUuid) {
        const created = await createCoolifyPrivateKey({
          name: `vibn-${workspace.slug}-gitea`,
          description: `Workspace ${workspace.slug} Gitea deploy key (${kp.fingerprint})`,
          privateKeyPem: kp.privateKeyPem,
        });
        coolifyPrivateKeyUuid = created.uuid;
      }
    } catch (err) {
      const msg = err instanceof Error ? err.message : String(err);
      errors.push(`ssh-key: ${msg}`);
      console.error('[workspaces] SSH deploy-key provisioning failed', workspace.slug, msg);
    }
  }

  const allReady = !!(
    coolifyUuid &&
    giteaOrg &&
    botUsername &&
    botTokenEncrypted &&
    coolifyPrivateKeyUuid &&
    giteaBotSshKeyId
  );
  const status: VibnWorkspace['provision_status'] =
    allReady ? 'ready' : errors.length > 0 ? 'partial' : 'pending';

  const updated = await query<VibnWorkspace>(
    `UPDATE vibn_workspaces
     SET coolify_project_uuid      = COALESCE($2, coolify_project_uuid),
         gitea_org                 = COALESCE($3, gitea_org),
         gitea_bot_username        = COALESCE($4, gitea_bot_username),
         gitea_bot_user_id         = COALESCE($5, gitea_bot_user_id),
         gitea_bot_token_encrypted = COALESCE($6, gitea_bot_token_encrypted),
         coolify_server_uuid       = COALESCE($7, coolify_server_uuid),
         coolify_destination_uuid  = COALESCE($8, coolify_destination_uuid),
         coolify_private_key_uuid  = COALESCE($9, coolify_private_key_uuid),
         gitea_bot_ssh_key_id      = COALESCE($10, gitea_bot_ssh_key_id),
         provision_status          = $11,
         provision_error           = $12,
         updated_at                = now()
     WHERE id = $1
     RETURNING *`,
    [
      workspace.id,
      coolifyUuid,
      giteaOrg,
      botUsername,
      botUserId,
      botTokenEncrypted,
      coolifyServerUuid,
      coolifyDestinationUuid,
      coolifyPrivateKeyUuid,
      giteaBotSshKeyId,
      status,
      errors.length ? errors.join('; ') : null,
    ]
  );

  return updated[0];
}

/**
 * Decrypt and return the bot credentials for a workspace. Call this
 * from endpoints that need to hand the AI a usable git clone URL.
 * Returns null when the workspace has not been fully provisioned.
 */
export function getWorkspaceBotCredentials(workspace: VibnWorkspace): {
  username: string;
  token: string;
  org: string;
} | null {
  if (!workspace.gitea_bot_username || !workspace.gitea_bot_token_encrypted || !workspace.gitea_org) {
    return null;
  }
  try {
    return {
      username: workspace.gitea_bot_username,
      token: decryptSecret(workspace.gitea_bot_token_encrypted),
      org: workspace.gitea_org,
    };
  } catch (err) {
    console.error('[workspaces] Failed to decrypt bot token for', workspace.slug, err);
    return null;
  }
}

/**
 * Convenience: get-or-create + provision in one call. Used by the
 * project-create flow so the first project in a fresh account always
 * has somewhere to land.
 */
export async function getOrCreateProvisionedWorkspace(opts: {
  userId: string;
  email: string;
  displayName?: string | null;
}): Promise<VibnWorkspace> {
  const ws = await ensureWorkspaceForUser(opts);
  return ensureWorkspaceProvisioned(ws);
}

// ──────────────────────────────────────────────────
// Slug uniqueness
// ──────────────────────────────────────────────────

async function pickAvailableSlug(base: string): Promise<string> {
  // Try base, then base-2, base-3, … up to base-99.
  for (let i = 0; i < 100; i++) {
    const candidate = i === 0 ? base : `${base}-${i + 1}`;
    const existing = await queryOne<{ id: string }>(
      `SELECT id FROM vibn_workspaces WHERE slug = $1 LIMIT 1`,
      [candidate]
    );
    if (!existing) return candidate;
  }
  // Extremely unlikely fallback
  return `${base}-${Date.now().toString(36)}`;
}
