/**
 * GA4 Data API v1beta Client — minimaler Wrapper für On-Demand-Session-Lookups.
 *
 * Wir nutzen bewusst KEINE google-auth-library, sondern signieren das JWT mit
 * Node's eingebautem `node:crypto`. Vorteile:
 *  - Keine zusätzliche Dep
 *  - Vollständige Kontrolle über Token-Lebensdauer und Cache
 *  - Minimales Attack-Surface
 *
 * Pro Workspace wird ein separater Service-Account hinterlegt (workspaces.ga4CredentialsJson)
 * und ein Token-Cache mit 50-Min-TTL gehalten.
 */

import { createSign } from 'node:crypto';

export interface Ga4ServiceAccount {
  client_email: string;
  private_key: string;
  project_id?: string;
  token_uri?: string;
}

interface CachedToken {
  accessToken: string;
  expiresAt: number;
}

// In-Memory-Cache pro Service-Account-Email (verschiedene Workspaces isoliert)
const tokenCache = new Map<string, CachedToken>();

const SCOPE = 'https://www.googleapis.com/auth/analytics.readonly';
const DEFAULT_TOKEN_URI = 'https://oauth2.googleapis.com/token';

function base64url(buf: Buffer): string {
  return buf.toString('base64').replace(/=+$/, '').replace(/\+/g, '-').replace(/\//g, '_');
}

/**
 * Signiert das Service-Account-JWT mit RS256. Liefert das vollständige JWT-Triple
 * `<header>.<payload>.<signature>` (Base64URL-kodiert).
 */
function signJwt(creds: Ga4ServiceAccount): string {
  const header = base64url(Buffer.from(JSON.stringify({ alg: 'RS256', typ: 'JWT' })));
  const now = Math.floor(Date.now() / 1000);
  const payload = base64url(Buffer.from(JSON.stringify({
    iss: creds.client_email,
    scope: SCOPE,
    aud: creds.token_uri ?? DEFAULT_TOKEN_URI,
    exp: now + 3600,
    iat: now,
  })));
  const signingInput = `${header}.${payload}`;
  const signature = createSign('RSA-SHA256').update(signingInput).sign(creds.private_key);
  return `${signingInput}.${base64url(signature)}`;
}

/**
 * Tauscht das JWT gegen einen Google-Access-Token. Cached pro Workspace-Email für
 * 50 Minuten (Token läuft nach 60min ab, wir lassen 10min Sicherheitsmarge).
 */
async function getAccessToken(creds: Ga4ServiceAccount): Promise<string> {
  const cacheKey = creds.client_email;
  const cached = tokenCache.get(cacheKey);
  if (cached && Date.now() < cached.expiresAt - 60_000) return cached.accessToken;

  const jwt = signJwt(creds);
  const tokenUri = creds.token_uri ?? DEFAULT_TOKEN_URI;

  const res = await fetch(tokenUri, {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
      assertion: jwt,
    }).toString(),
  });

  if (!res.ok) {
    const txt = await res.text().catch(() => '');
    throw new Error(`GA4 token exchange failed (${res.status}): ${txt.slice(0, 200)}`);
  }

  const data = (await res.json()) as { access_token: string; expires_in: number };
  const accessToken = data.access_token;
  const expiresAt = Date.now() + (data.expires_in ?? 3600) * 1000;
  tokenCache.set(cacheKey, { accessToken, expiresAt });
  return accessToken;
}

export interface RunReportRequest {
  propertyId: string;
  dimensions?: string[];
  metrics?: string[];
  dateRanges?: Array<{ startDate: string; endDate: string }>;
  dimensionFilter?: unknown;
  limit?: number;
  orderBys?: unknown[];
}

interface RawDimensionValue {
  value?: string;
}
interface RawMetricValue {
  value?: string;
}
interface RawRow {
  dimensionValues?: RawDimensionValue[];
  metricValues?: RawMetricValue[];
}
interface RawResponse {
  dimensionHeaders?: Array<{ name: string }>;
  metricHeaders?: Array<{ name: string }>;
  rows?: RawRow[];
  rowCount?: number;
}

export interface RunReportResult {
  dimensionHeaders: string[];
  metricHeaders: string[];
  rows: Array<{
    dimensions: string[];
    metrics: string[];
  }>;
  rowCount: number;
}

/**
 * Führt einen GA4 runReport-Call aus. Authentifiziert sich automatisch über
 * den Service-Account (Caller muss creds passen).
 *
 * propertyId ohne 'properties/'-Prefix übergeben — wir wrappen das hier.
 */
export async function runReport(creds: Ga4ServiceAccount, req: RunReportRequest): Promise<RunReportResult> {
  const token = await getAccessToken(creds);
  const url = `https://analyticsdata.googleapis.com/v1beta/properties/${encodeURIComponent(req.propertyId)}:runReport`;

  const body = {
    dimensions: (req.dimensions ?? []).map((name) => ({ name })),
    metrics: (req.metrics ?? []).map((name) => ({ name })),
    dateRanges: req.dateRanges ?? [{ startDate: '7daysAgo', endDate: 'today' }],
    dimensionFilter: req.dimensionFilter,
    limit: req.limit ?? 100,
    orderBys: req.orderBys,
  };

  const res = await fetch(url, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${token}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(body),
  });

  if (!res.ok) {
    const txt = await res.text().catch(() => '');
    throw new Error(`GA4 runReport failed (${res.status}): ${txt.slice(0, 300)}`);
  }

  const raw = (await res.json()) as RawResponse;
  return {
    dimensionHeaders: (raw.dimensionHeaders ?? []).map((h) => h.name),
    metricHeaders: (raw.metricHeaders ?? []).map((h) => h.name),
    rows: (raw.rows ?? []).map((r) => ({
      dimensions: (r.dimensionValues ?? []).map((d) => d.value ?? ''),
      metrics: (r.metricValues ?? []).map((m) => m.value ?? ''),
    })),
    rowCount: raw.rowCount ?? 0,
  };
}

/**
 * High-Level-Helper: Sucht die GA4-Session(s) eines Recipients über
 * die mitgesendete utm_content-ID. Wir filtern auf sessionCampaignContent
 * exakt-match auf "<variantLabel>-<recipientId>".
 *
 * Returns null wenn keine Session gefunden — Caller zeigt dann "Keine Session"-Box.
 */
export interface RecipientSessionSummary {
  sessions: number;
  pageviews: number;
  engagedSessions: number;
  averageEngagementDurationSec: number;
  conversions: number;
  topPages: Array<{ path: string; views: number }>;
}

export async function getSessionForRecipient(
  creds: Ga4ServiceAccount,
  propertyId: string,
  utmContentMatch: string,
  lookbackDays = 30,
): Promise<RecipientSessionSummary | null> {
  const dateRange = {
    startDate: `${lookbackDays}daysAgo`,
    endDate: 'today',
  };

  // Aggregat-Metriken
  const summary = await runReport(creds, {
    propertyId,
    dimensions: ['sessionCampaignContent'],
    metrics: ['sessions', 'screenPageViews', 'engagedSessions', 'userEngagementDuration', 'conversions'],
    dateRanges: [dateRange],
    dimensionFilter: {
      filter: {
        fieldName: 'sessionCampaignContent',
        stringFilter: { matchType: 'EXACT', value: utmContentMatch },
      },
    },
    limit: 1,
  });

  if (summary.rowCount === 0) return null;

  const m = summary.rows[0]?.metrics ?? [];
  const sessions = Number(m[0] ?? 0);
  const pageviews = Number(m[1] ?? 0);
  const engagedSessions = Number(m[2] ?? 0);
  const engagementDurationSec = Number(m[3] ?? 0);
  const conversions = Number(m[4] ?? 0);

  // Top-Pages für den Drilldown
  const pages = await runReport(creds, {
    propertyId,
    dimensions: ['pagePath'],
    metrics: ['screenPageViews'],
    dateRanges: [dateRange],
    dimensionFilter: {
      filter: {
        fieldName: 'sessionCampaignContent',
        stringFilter: { matchType: 'EXACT', value: utmContentMatch },
      },
    },
    orderBys: [{ metric: { metricName: 'screenPageViews' }, desc: true }],
    limit: 10,
  });

  return {
    sessions,
    pageviews,
    engagedSessions,
    averageEngagementDurationSec: sessions > 0 ? engagementDurationSec / sessions : 0,
    conversions,
    topPages: pages.rows.map((r) => ({
      path: r.dimensions[0] ?? '',
      views: Number(r.metrics[0] ?? 0),
    })),
  };
}

/**
 * Validiert ein Service-Account-JSON. Wirft beim ersten fehlenden Feld.
 * Wird im Settings-UI vor dem Speichern aufgerufen, um inkorrekte Uploads
 * sofort abzufangen.
 */
export function validateServiceAccount(raw: unknown): Ga4ServiceAccount {
  if (!raw || typeof raw !== 'object') throw new Error('Service-Account-JSON muss ein Objekt sein');
  const obj = raw as Record<string, unknown>;
  if (typeof obj.client_email !== 'string' || !obj.client_email.includes('@')) {
    throw new Error('client_email fehlt oder ist ungültig');
  }
  if (typeof obj.private_key !== 'string' || !obj.private_key.includes('BEGIN PRIVATE KEY')) {
    throw new Error('private_key fehlt oder ist kein PEM-Format');
  }
  return {
    client_email: obj.client_email,
    private_key: obj.private_key,
    project_id: typeof obj.project_id === 'string' ? obj.project_id : undefined,
    token_uri: typeof obj.token_uri === 'string' ? obj.token_uri : undefined,
  };
}

/**
 * Baut den utm_content-Match-String, der vom Worker beim Send in die URL
 * eingebaut wird. Format: "<variantLabel>-<recipientId>" — auf 80 Zeichen
 * gekappt, weil GA4 customDimensions diese Länge erwartet.
 *
 * Beispiel: 'A-9c8d7e6f-1234-5678-90ab-cdef12345678'
 */
export function buildUtmContentMatch(variantLabel: string | null, recipientId: string): string {
  const prefix = variantLabel ? `${variantLabel}-` : '';
  return (prefix + recipientId).slice(0, 80);
}
