/**
 * Sentry-as-product integration.
 *
 * Provisions a Sentry project per Vibn project under the shared
 * `vibnai` Sentry org, then makes its DSN + auth token available
 * to any Coolify app deployed for that project. Real-user errors
 * from the user's deployed app land in Sentry; AI tools (Stage 3)
 * read the issue feed back into chat (Stage 4).
 *
 * Design choices:
 *  - **Idempotent.** Looks up existing Sentry projects by slug
 *    before creating, so reruns are safe.
 *  - **Shared org token.** The same `SENTRY_AUTH_TOKEN` we use to
 *    upload source maps for vibn-frontend is reused across every
 *    user project — it has org-write scope. This means we DON'T
 *    need per-project auth tokens, just per-project DSNs.
 *  - **Slug convention.** `vibn-{workspace}-{projectSlug}`. Avoids
 *    collisions across workspaces and stays under Sentry's
 *    50-char project slug limit. Clamped if longer.
 *  - **Soft failure.** If Sentry provisioning fails (rate limit,
 *    network, token revoked) we log and continue. The Vibn
 *    project still works without Sentry; we'll lazily retry on
 *    the next deploy.
 *
 * See SENTRY_AS_PRODUCT.md for the full proposal context.
 */

import { query } from '@/lib/db-postgres';
import { upsertApplicationEnv } from '@/lib/coolify';

const SENTRY_API_BASE = 'https://de.sentry.io/api/0';
const SENTRY_ORG_SLUG = 'vibnai';
const SENTRY_TEAM_SLUG = 'vibnai'; // Default team name matches org slug for personal/single-team setups.

export interface SentryProjectInfo {
  /** Sentry project slug, e.g. `vibn-mark-account-checkout-app`. */
  slug: string;
  /** Public DSN for runtime error capture. NEXT_PUBLIC_SENTRY_DSN. */
  dsn: string;
  /** When this row was created/updated. */
  provisionedAt: string;
}

export interface ProvisionInput {
  projectId: string;
  /** Vibn workspace slug, e.g. "mark-account". */
  workspaceSlug: string;
  /** Vibn project slug, e.g. "checkout-app". */
  projectSlug: string;
  /** Display name shown in the Sentry UI. */
  projectName: string;
}

/**
 * Returns the Sentry project for a given Vibn project, creating
 * it if missing. Persists the result to fs_projects.data.sentry
 * so subsequent calls hit the cache instead of Sentry's API.
 *
 * Returns null if Sentry provisioning fails — caller should log
 * and continue.
 */
export async function ensureSentryProject(
  input: ProvisionInput,
): Promise<SentryProjectInfo | null> {
  const authToken = process.env.SENTRY_AUTH_TOKEN;
  if (!authToken) {
    console.warn('[sentry] SENTRY_AUTH_TOKEN missing — skipping provisioning');
    return null;
  }

  // 1. Fast path: already provisioned for this Vibn project.
  const cached = await loadSentryFromProject(input.projectId);
  if (cached) return cached;

  // 2. Build the deterministic slug. Sentry caps at 50 chars and
  //    only allows lowercase a–z, 0–9, dashes.
  const slug = buildSentrySlug(input.workspaceSlug, input.projectSlug);

  try {
    // 3. Try to look up existing Sentry project (handles the case
    //    where fs_projects was wiped but Sentry still has the project).
    const existing = await fetchSentryProject(slug, authToken);
    if (existing) {
      const info = await materialize(input.projectId, existing, authToken);
      if (info) return info;
    }

    // 4. Create fresh.
    const created = await createSentryProject({
      slug,
      name: input.projectName.slice(0, 50),
      authToken,
    });
    if (!created) return null;

    return await materialize(input.projectId, created, authToken);
  } catch (err) {
    console.error('[sentry] ensureSentryProject failed', err);
    return null;
  }
}

/**
 * Sets the standard Sentry env vars on a Coolify application
 * so that its next build inlines the DSN and uploads source maps.
 * Idempotent — safe to call on every apps.create. Soft-fails on
 * any per-key error so a single failed upsert doesn't block deploy.
 *
 * Env vars set:
 *   - NEXT_PUBLIC_SENTRY_DSN — public DSN for runtime capture
 *   - SENTRY_AUTH_TOKEN     — shared org token for source map upload
 *
 * Both are marked Coolify-default (is_buildtime=true is_runtime=true);
 * the public DSN must be present at build time so Next.js inlines
 * it into the client bundle.
 */
export async function applySentryEnvToCoolifyApp(
  coolifyAppUuid: string,
  projectId: string,
): Promise<void> {
  const sentry = await loadSentryFromProject(projectId);
  if (!sentry) return; // Project not yet provisioned in Sentry — Stage 1 will retry.

  const authToken = process.env.SENTRY_AUTH_TOKEN;
  if (!authToken) return;

  const envs: Array<[string, string]> = [
    ['NEXT_PUBLIC_SENTRY_DSN', sentry.dsn],
    ['SENTRY_AUTH_TOKEN', authToken],
  ];

  for (const [key, value] of envs) {
    try {
      await upsertApplicationEnv(coolifyAppUuid, { key, value });
    } catch (err) {
      console.warn(`[sentry] upsert ${key} on ${coolifyAppUuid} failed`, err);
    }
  }
}

/**
 * Returns the Sentry project info for a Vibn project if already
 * provisioned, else null. Read-only — does NOT call Sentry's API.
 */
export async function loadSentryFromProject(
  projectId: string,
): Promise<SentryProjectInfo | null> {
  const rows = await query<{ data: any }>(
    `SELECT data FROM fs_projects WHERE id = $1 LIMIT 1`,
    [projectId],
  );
  const sentry = rows[0]?.data?.sentry;
  if (sentry?.slug && sentry?.dsn && sentry?.provisionedAt) {
    return sentry as SentryProjectInfo;
  }
  return null;
}

// ──────────────────────────────────────────────────────────────────
// Issue feed — Stage 3 MCP tools read these
// ──────────────────────────────────────────────────────────────────

export interface SentryIssueSummary {
  id: string;
  title: string;
  /** "error" | "fatal" | "warning" | "info" | "debug" */
  level: string;
  /** Total event count (lifetime). */
  count: number;
  /** ISO of most recent occurrence. */
  lastSeen: string;
  /** ISO of first occurrence. */
  firstSeen: string;
  /** "unresolved" | "resolved" | "ignored" — we filter to unresolved. */
  status: string;
  /** Best-effort culprit string from Sentry, e.g. "GET /api/checkout". */
  culprit: string | null;
  /** Direct URL to the issue in Sentry's UI. */
  permalink: string | null;
}

export interface SentryEventDetail {
  /** Sentry event ID (the canonical "fingerprint" of this occurrence). */
  eventId: string;
  /** Most recent event's render of the stack trace (top frames first). */
  stackFrames: Array<{
    function: string | null;
    filename: string | null;
    lineno: number | null;
    colno: number | null;
    /** Source code surrounding the line if Sentry has source maps. */
    contextLine: string | null;
  }>;
  /** User-context tag (email/id/username/ip), if Sentry captured one. */
  user: { email?: string; id?: string; username?: string; ipAddress?: string } | null;
  /** Request method + URL if this was an HTTP-facing error. */
  request: { method?: string; url?: string } | null;
  /** Truncated breadcrumbs (last 20) — clicks, fetches, navigations. */
  breadcrumbs: Array<{
    type: string | null;
    category: string | null;
    message: string | null;
    timestamp: string | null;
  }>;
  /** Direct URL to a Session Replay if the user had one recorded. */
  replayUrl: string | null;
}

/**
 * List recent unresolved issues for a Vibn project's Sentry project.
 * Returns [] if Sentry isn't provisioned yet — caller should treat
 * that as "no errors", which is functionally correct (no Sentry =
 * no error capture path).
 */
export async function listRecentSentryIssues(
  projectId: string,
  options?: { limit?: number; sinceHours?: number },
): Promise<SentryIssueSummary[]> {
  const sentry = await loadSentryFromProject(projectId);
  if (!sentry) return [];

  const authToken = process.env.SENTRY_AUTH_TOKEN;
  if (!authToken) return [];

  const limit = clampLimit(options?.limit ?? 10);
  const sinceHours = options?.sinceHours ?? 24;
  const statsPeriod = `${sinceHours}h`;

  const url = new URL(
    `${SENTRY_API_BASE}/projects/${SENTRY_ORG_SLUG}/${sentry.slug}/issues/`,
  );
  url.searchParams.set('limit', String(limit));
  url.searchParams.set('query', 'is:unresolved');
  url.searchParams.set('statsPeriod', statsPeriod);
  url.searchParams.set('sort', 'date');

  const res = await fetch(url, {
    headers: { Authorization: `Bearer ${authToken}` },
  });
  if (!res.ok) {
    console.warn(
      `[sentry] listRecentSentryIssues ${res.status}: ${await res.text()}`,
    );
    return [];
  }
  const raw = (await res.json()) as Array<Record<string, any>>;
  return raw.map((i) => ({
    id: String(i.id),
    title: String(i.title ?? '(untitled)'),
    level: String(i.level ?? 'error'),
    count: Number(i.count ?? 0),
    lastSeen: String(i.lastSeen ?? ''),
    firstSeen: String(i.firstSeen ?? ''),
    status: String(i.status ?? 'unresolved'),
    culprit: i.culprit ? String(i.culprit) : null,
    permalink: i.permalink ? String(i.permalink) : null,
  }));
}

/**
 * Fetch the latest event for an issue, with stack trace,
 * breadcrumbs, user, request, and Session Replay link if any.
 * Returns null if the issue isn't found or Sentry isn't ready.
 */
export async function getSentryIssueDetail(
  projectId: string,
  issueId: string,
): Promise<SentryEventDetail | null> {
  const sentry = await loadSentryFromProject(projectId);
  if (!sentry) return null;
  const authToken = process.env.SENTRY_AUTH_TOKEN;
  if (!authToken) return null;

  const res = await fetch(
    `${SENTRY_API_BASE}/issues/${encodeURIComponent(issueId)}/events/latest/`,
    {
      headers: { Authorization: `Bearer ${authToken}` },
    },
  );
  if (!res.ok) {
    console.warn(
      `[sentry] getSentryIssueDetail ${res.status}: ${await res.text()}`,
    );
    return null;
  }
  const e = (await res.json()) as Record<string, any>;

  // Sentry stores frames in a few different places depending on
  // platform. The exception entry has the most reliable shape.
  const exceptionEntry = (e.entries || []).find(
    (entry: any) => entry?.type === 'exception',
  );
  const firstException = exceptionEntry?.data?.values?.[0];
  const frames =
    (firstException?.stacktrace?.frames as Array<Record<string, any>> | undefined) ?? [];
  // Sentry returns frames in oldest-first order; reverse so top of
  // stack (the line that actually threw) is first — which is what
  // every developer scans first.
  const stackFrames = [...frames].reverse().slice(0, 12).map((f) => ({
    function: f.function ?? null,
    filename: f.filename ?? null,
    lineno: typeof f.lineno === 'number' ? f.lineno : null,
    colno: typeof f.colno === 'number' ? f.colno : null,
    contextLine: f.contextLine ?? null,
  }));

  const breadcrumbsEntry = (e.entries || []).find(
    (entry: any) => entry?.type === 'breadcrumbs',
  );
  const breadcrumbs =
    ((breadcrumbsEntry?.data?.values as Array<Record<string, any>> | undefined) ?? [])
      .slice(-20)
      .map((b) => ({
        type: b.type ?? null,
        category: b.category ?? null,
        message: b.message ?? null,
        timestamp: b.timestamp ?? null,
      }));

  const requestEntry = (e.entries || []).find(
    (entry: any) => entry?.type === 'request',
  );
  const request = requestEntry?.data
    ? {
        method: requestEntry.data.method ?? undefined,
        url: requestEntry.data.url ?? undefined,
      }
    : null;

  // Session Replay: Sentry attaches a `replayId` tag on events that
  // have one. Build the canonical UI URL.
  const replayId = (e.tags as Array<Record<string, any>> | undefined)?.find(
    (t) => t.key === 'replayId',
  )?.value;
  const replayUrl = replayId
    ? `https://${SENTRY_ORG_SLUG}.sentry.io/replays/${replayId}/`
    : null;

  return {
    eventId: String(e.eventID ?? e.id ?? ''),
    stackFrames,
    user: e.user ? sanitizeUser(e.user) : null,
    request,
    breadcrumbs,
    replayUrl,
  };
}

/**
 * Mark an issue resolved. Used by the AI after it ships a fix and
 * verifies the bug no longer fires (e.g. via tests, log watch, or
 * explicit user confirmation).
 */
export async function resolveSentryIssue(
  projectId: string,
  issueId: string,
): Promise<boolean> {
  const sentry = await loadSentryFromProject(projectId);
  if (!sentry) return false;
  const authToken = process.env.SENTRY_AUTH_TOKEN;
  if (!authToken) return false;

  const res = await fetch(
    `${SENTRY_API_BASE}/issues/${encodeURIComponent(issueId)}/`,
    {
      method: 'PUT',
      headers: {
        Authorization: `Bearer ${authToken}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ status: 'resolved' }),
    },
  );
  return res.ok;
}

function clampLimit(n: number): number {
  if (!Number.isFinite(n)) return 10;
  return Math.max(1, Math.min(50, Math.floor(n)));
}

function sanitizeUser(u: Record<string, any>): SentryEventDetail['user'] {
  return {
    email: u.email ?? undefined,
    id: u.id ?? undefined,
    username: u.username ?? undefined,
    ipAddress: u.ip_address ?? u.ipAddress ?? undefined,
  };
}

// ──────────────────────────────────────────────────────────────────
// Internal helpers
// ──────────────────────────────────────────────────────────────────

function buildSentrySlug(workspaceSlug: string, projectSlug: string): string {
  const raw = `vibn-${workspaceSlug}-${projectSlug}`
    .toLowerCase()
    .replace(/[^a-z0-9-]+/g, '-')
    .replace(/-+/g, '-')
    .replace(/^-|-$/g, '');
  // Sentry: 50 char max. Truncate from the end (preserves the
  // `vibn-{workspace}` prefix which keeps slugs distinguishable
  // across workspaces).
  return raw.slice(0, 50);
}

interface SentryApiProject {
  slug: string;
  name: string;
  id: string;
}

async function fetchSentryProject(
  slug: string,
  authToken: string,
): Promise<SentryApiProject | null> {
  const res = await fetch(
    `${SENTRY_API_BASE}/projects/${SENTRY_ORG_SLUG}/${slug}/`,
    {
      headers: { Authorization: `Bearer ${authToken}` },
    },
  );
  if (res.status === 404) return null;
  if (!res.ok) {
    throw new Error(
      `Sentry GET project failed: ${res.status} ${await res.text()}`,
    );
  }
  return (await res.json()) as SentryApiProject;
}

async function createSentryProject(input: {
  slug: string;
  name: string;
  authToken: string;
}): Promise<SentryApiProject | null> {
  const res = await fetch(
    `${SENTRY_API_BASE}/teams/${SENTRY_ORG_SLUG}/${SENTRY_TEAM_SLUG}/projects/`,
    {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${input.authToken}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        name: input.name,
        slug: input.slug,
        platform: 'javascript-nextjs', // Most common case; doesn't gate which SDK actually sends events.
      }),
    },
  );
  if (!res.ok) {
    const text = await res.text();
    // 409 = slug already exists in another team; we tried the GET
    // path first, so this is genuinely a race or a slug conflict.
    if (res.status === 409) {
      console.warn(`[sentry] slug ${input.slug} taken — retry GET`);
      return await fetchSentryProject(input.slug, input.authToken);
    }
    throw new Error(`Sentry POST project failed: ${res.status} ${text}`);
  }
  return (await res.json()) as SentryApiProject;
}

interface SentryClientKey {
  dsn: { public: string };
  isActive: boolean;
}

async function fetchProjectDsn(
  slug: string,
  authToken: string,
): Promise<string | null> {
  const res = await fetch(
    `${SENTRY_API_BASE}/projects/${SENTRY_ORG_SLUG}/${slug}/keys/`,
    {
      headers: { Authorization: `Bearer ${authToken}` },
    },
  );
  if (!res.ok) {
    throw new Error(
      `Sentry GET keys failed: ${res.status} ${await res.text()}`,
    );
  }
  const keys = (await res.json()) as SentryClientKey[];
  // Sentry auto-creates a "Default" key on project creation; pick
  // the first active one. Multiple keys exist in pro setups but
  // we always want the live one.
  const active = keys.find((k) => k.isActive) ?? keys[0];
  return active?.dsn?.public ?? null;
}

async function materialize(
  projectId: string,
  apiProj: SentryApiProject,
  authToken: string,
): Promise<SentryProjectInfo | null> {
  const dsn = await fetchProjectDsn(apiProj.slug, authToken);
  if (!dsn) {
    console.error(`[sentry] no DSN returned for ${apiProj.slug}`);
    return null;
  }

  const info: SentryProjectInfo = {
    slug: apiProj.slug,
    dsn,
    provisionedAt: new Date().toISOString(),
  };

  await query(
    `UPDATE fs_projects
        SET data = jsonb_set(COALESCE(data, '{}'::jsonb), '{sentry}', $2::jsonb, true),
            updated_at = NOW()
      WHERE id = $1`,
    [projectId, JSON.stringify(info)],
  );

  return info;
}
