/**
 * Vibn projects — per-project resource isolation.
 *
 * Each Vibn project lives inside a Workspace and owns its OWN Coolify Project
 * (named `vibn-{workspace-slug}-{project-slug}`). All apps, databases, and
 * services deployed for the project land inside that Coolify project, giving
 * us:
 *   - clean grouping in the Coolify UI
 *   - cascading delete (drop project → drop all its resources)
 *   - per-project billing/usage attribution
 *   - per-project domain namespace (`*.{project-slug}.{workspace-slug}.vibnai.com`)
 *
 * The mapping is stored on fs_projects.data.coolifyProjectUuid. Helpers below
 * are idempotent — safe to call repeatedly.
 */

import { query, queryOne } from '@/lib/db-postgres';
import { createProject as createCoolifyProject, listProjects as listCoolifyProjects } from '@/lib/coolify';
import type { VibnWorkspace } from '@/lib/workspaces';

export interface VibnProjectRow {
  id: string;
  data: any;
  workspace: string;
  slug: string;
  user_id: string;
  vibn_workspace_id: string | null;
  created_at: Date;
  updated_at: Date;
}

/** Coolify Project name we use for a Vibn project. */
export function coolifyProjectNameForVibnProject(workspaceSlug: string, projectSlug: string): string {
  return `vibn-${workspaceSlug}-${projectSlug}`;
}

/** Coolify's `description` validator only accepts a narrow set of chars
 *  (letters, numbers, spaces, and `- _ . , ! ? ( ) ' " + = * / @ &`).
 *  Notably colon, semicolon, brackets, and pipe are rejected. We strip
 *  anything else down to a hyphen so create payloads always validate. */
export function sanitizeCoolifyDescription(s: string): string {
  return s
    .replace(/[^A-Za-z0-9 \-_.,!?()'"+=*/@&]/g, "-")
    .replace(/-{2,}/g, "-")
    .trim()
    .slice(0, 255);
}

/**
 * Idempotently ensure the given Vibn project has its own Coolify Project.
 * Returns the Coolify project UUID. Persists it to fs_projects.data.coolifyProjectUuid.
 *
 * - If already stored, returns immediately.
 * - If not stored, looks up by name in Coolify (handles re-runs after a
 *   half-failed previous create) and either reuses or creates fresh.
 * - Falls back to the workspace's legacy `vibn-ws-{slug}` project on Coolify
 *   failure so deploys aren't blocked.
 */
export async function ensureProjectCoolifyProject(
  projectId: string,
  workspace: VibnWorkspace,
  opts: { projectSlug: string; projectName?: string },
): Promise<string | null> {
  const row = await queryOne<{ data: any }>(
    `SELECT data FROM fs_projects WHERE id = $1 LIMIT 1`,
    [projectId],
  );
  if (!row) return null;

  const stored = row.data?.coolifyProjectUuid as string | undefined;

  // SAFETY: if a prior bug stored the *workspace*'s Coolify project UUID
  // here, treat it as missing and re-provision. Sharing the workspace
  // Coolify project across Vibn projects causes services from one
  // project to bleed into another (we hit this on Apr 29).
  const isWorkspaceUuid =
    stored && workspace.coolify_project_uuid && stored === workspace.coolify_project_uuid;
  if (stored && !isWorkspaceUuid) return stored;
  if (isWorkspaceUuid) {
    console.warn(
      '[projects] Detected workspace-UUID stored as project Coolify UUID for',
      projectId, '— re-provisioning a dedicated project.'
    );
  }

  const wantName = coolifyProjectNameForVibnProject(workspace.slug, opts.projectSlug);
  const description = sanitizeCoolifyDescription(
    `Vibn project ${opts.projectName || opts.projectSlug} - workspace ${workspace.slug}`,
  );

  let coolifyUuid: string | null = null;
  try {
    // First check if it already exists (could happen if a previous create call
    // succeeded on Coolify but failed before persisting back to fs_projects).
    const all = await listCoolifyProjects();
    const existing = all.find((p) => p.name === wantName);
    if (existing) {
      coolifyUuid = existing.uuid;
    } else {
      const created = await createCoolifyProject(wantName, description);
      coolifyUuid = created.uuid;
    }
  } catch (err) {
    console.error(
      '[projects] Failed to provision Coolify project for',
      projectId,
      err instanceof Error ? err.message : String(err),
    );
    // Do NOT fall back to the workspace project. Sharing it across Vibn
    // projects leaks every project's services into every other project
    // (the bug this branch was originally written to "soften"). Better
    // to leave coolifyProjectUuid null — the UI surfaces an empty state
    // and the user can retry once the underlying error is fixed.
    coolifyUuid = null;
  }

  if (coolifyUuid) {
    await query(
      `UPDATE fs_projects
         SET data = data || jsonb_build_object('coolifyProjectUuid', $2::text),
             updated_at = NOW()
       WHERE id = $1`,
      [projectId, coolifyUuid],
    );
  }

  return coolifyUuid;
}

/**
 * Resolve the Coolify project UUID for a given Vibn project ID, scoped to
 * the workspace. Returns null if the project doesn't exist or doesn't belong
 * to the workspace.
 */
export async function getProjectCoolifyUuid(
  projectId: string,
  workspace: VibnWorkspace,
): Promise<string | null> {
  const row = await queryOne<{ data: any }>(
    `SELECT data
       FROM fs_projects
      WHERE id = $1
        AND (vibn_workspace_id = $2 OR workspace = $3)
      LIMIT 1`,
    [projectId, workspace.id, workspace.slug],
  );
  if (!row) return null;
  return (row.data?.coolifyProjectUuid as string) || null;
}

/**
 * Return the COMPLETE set of Coolify project UUIDs owned by this workspace —
 * the workspace's legacy `vibn-ws-{slug}` project PLUS every per-Vibn-project
 * Coolify project that has been provisioned. Used by the tenant safety gate
 * so an API-key principal can touch any of its workspace's resources but
 * absolutely nothing outside.
 */
export async function getOwnedCoolifyProjectUuids(workspace: VibnWorkspace): Promise<Set<string>> {
  const rows = await query<{ uuid: string }>(
    `SELECT DISTINCT data->>'coolifyProjectUuid' AS uuid
       FROM fs_projects
      WHERE (vibn_workspace_id = $1 OR workspace = $2)
        AND data->>'coolifyProjectUuid' IS NOT NULL`,
    [workspace.id, workspace.slug],
  );
  const set = new Set<string>();
  for (const r of rows) if (r.uuid) set.add(r.uuid);
  if (workspace.coolify_project_uuid) set.add(workspace.coolify_project_uuid);
  return set;
}

// ──────────────────────────────────────────────────
// Per-resource project ownership (fs_project_resources)
// ──────────────────────────────────────────────────
//
// Coolify groups resources by `project` natively, but a single Coolify project
// can legitimately host multiple Vibn projects (the legacy `vibn-ws-{slug}`
// shared project, or hand-grouped scratch projects). We track the
// authoritative Vibn-project ↔ Coolify-resource link in our own table so that:
//
//   - `apps_list { projectId }` returns ONLY the resources the user genuinely
//     owns under that Vibn project, even when the underlying Coolify project
//     mixes in unrelated services (e.g. Twenty CRM and n8n in the same legacy
//     workspace project).
//   - We can backfill legacy deployments without physically moving them in
//     Coolify (whose API doesn't cleanly support env reassignment).
//   - Cascading delete becomes a single SQL filter.

export type ResourceType = 'application' | 'service' | 'database';

let projectResourcesTableReady = false;
export async function ensureProjectResourcesTable(): Promise<void> {
  if (projectResourcesTableReady) return;
  await query(
    `CREATE TABLE IF NOT EXISTS fs_project_resources (
       project_id     TEXT NOT NULL,
       workspace      TEXT NOT NULL,
       resource_uuid  TEXT NOT NULL,
       resource_type  TEXT NOT NULL,
       created_at     TIMESTAMPTZ NOT NULL DEFAULT now(),
       PRIMARY KEY (project_id, resource_uuid)
     );
     CREATE INDEX IF NOT EXISTS fs_project_resources_uuid_idx
       ON fs_project_resources (resource_uuid);
     CREATE INDEX IF NOT EXISTS fs_project_resources_workspace_idx
       ON fs_project_resources (workspace, project_id);`,
    [],
  );
  projectResourcesTableReady = true;
}

export async function linkResourceToProject(
  projectId: string,
  workspace: string,
  resourceUuid: string,
  resourceType: ResourceType,
): Promise<void> {
  await ensureProjectResourcesTable();
  await query(
    `INSERT INTO fs_project_resources (project_id, workspace, resource_uuid, resource_type)
       VALUES ($1, $2, $3, $4)
       ON CONFLICT (project_id, resource_uuid) DO NOTHING`,
    [projectId, workspace, resourceUuid, resourceType],
  );
}

export async function unlinkResource(resourceUuid: string): Promise<void> {
  await ensureProjectResourcesTable();
  await query(`DELETE FROM fs_project_resources WHERE resource_uuid = $1`, [resourceUuid]);
}

/** All Coolify resource UUIDs explicitly linked to a Vibn project. */
export async function getProjectResourceUuids(
  projectId: string,
): Promise<Map<string, ResourceType>> {
  await ensureProjectResourcesTable();
  const rows = await query<{ resource_uuid: string; resource_type: ResourceType }>(
    `SELECT resource_uuid, resource_type FROM fs_project_resources WHERE project_id = $1`,
    [projectId],
  );
  const map = new Map<string, ResourceType>();
  for (const r of rows) map.set(r.resource_uuid, r.resource_type);
  return map;
}

/**
 * Reverse lookup: which Vibn project does this Coolify resource belong to?
 * Returns null when no explicit link exists (caller can fall back to
 * coolifyProjectUuid-based grouping).
 */
export async function getProjectIdForResource(
  resourceUuid: string,
  workspace: string,
): Promise<string | null> {
  await ensureProjectResourcesTable();
  const row = await queryOne<{ project_id: string }>(
    `SELECT project_id FROM fs_project_resources
      WHERE resource_uuid = $1 AND workspace = $2 LIMIT 1`,
    [resourceUuid, workspace],
  );
  return row?.project_id ?? null;
}
