import { getCategoryConsent } from '@/lib/cookieConsent';

import type { TypedAnalyticsEvent } from './analytics-types';
import { FILTER_APPLIED, LEGACY_EVENT_MAP } from './event-catalog';

/**
 * Check if the user has granted analytics consent.
 */
export function hasAnalyticsConsent(): boolean {
  return getCategoryConsent('analytics');
}

/**
 * GA4 event parameter name constraints:
 * - Max 40 characters
 * - Only letters, numbers, and underscores
 * - Must start with a letter or underscore (not a digit)
 */
const GA4_PARAM_NAME_MAX_LEN = 40;
const GA4_PARAM_NAME_VALID = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
const GA4_PARAM_NAME_SANITIZE = /[^a-zA-Z0-9_]/g;
const GA4_PARAM_NAME_LEADING_DIGIT = /^[0-9]/;

/**
 * Sanitize a single property name for GA4 compliance.
 * Returns the sanitized name, or null if the result would be empty/invalid.
 */
function sanitizeGa4ParamName(name: string): string | null {
  let sanitized = name.replace(GA4_PARAM_NAME_SANITIZE, '_');
  // If it starts with a digit, prepend underscore
  if (GA4_PARAM_NAME_LEADING_DIGIT.test(sanitized)) {
    sanitized = '_' + sanitized;
  }
  sanitized = sanitized.slice(0, GA4_PARAM_NAME_MAX_LEN);
  if (sanitized.length === 0 || sanitized === '_') {
    return null;
  }
  return sanitized;
}

/**
 * Return a copy of params with GA4-compliant property names.
 * Non-conforming names are sanitized (truncated, special chars replaced with _).
 * Warns in dev mode when sanitization was needed.
 */
function sanitizeForGa4(
  params: Record<string, string | number | boolean>,
): Record<string, string | number | boolean> {
  const sanitized: Record<string, string | number | boolean> = {};
  for (const [key, value] of Object.entries(params)) {
    if (GA4_PARAM_NAME_VALID.test(key) && key.length <= GA4_PARAM_NAME_MAX_LEN) {
      sanitized[key] = value;
    } else {
      const cleanKey = sanitizeGa4ParamName(key);
      if (cleanKey !== null) {
        sanitized[cleanKey] = value;
        if (isAnalyticsDebug() && import.meta.env.DEV) {
          console.warn('[analytics] GA4 param name sanitized:', key, '→', cleanKey);
        }
      }
    }
  }
  return sanitized;
}

/**
 * Forward an event to PostHog if the global `window.posthog` exists.
 */
function forwardToPostHog(
  eventName: string,
  params?: Record<string, string | number | boolean>,
): void {
  if (typeof window !== 'undefined' && window.posthog?.capture) {
    window.posthog.capture(eventName, params);
  }
}

/**
 * Track a custom event via GA4 (gtag). No-op when gtag is unavailable (dev/test)
 * or when the user has not granted analytics consent.
 *
 * Note: server-side events in AnalyticsEventService are split into two classes:
 *   - Legitimate interest (GDPR Art. 6(1)(f)): core funnel events (signup, activation,
 *     billing, analysis) that fire unconditionally regardless of this consent gate.
 *   - Consent-required: behavioral/profiling events (feature_used, filter_applied,
 *     notification_delivered) that are gated on the user's stored analytics consent
 *     preference in the cookie_consents table.
 */
export function trackEvent(
  eventName: string,
  params?: Record<string, string | number | boolean>,
): void {
  // Remap deprecated/legacy event names to their canonical equivalents at runtime.
  const resolvedName = LEGACY_EVENT_MAP[eventName] ?? eventName;

  if (isAnalyticsDebug() && import.meta.env.DEV) {
    // Log all events — including consent-blocked ones — so suppressions are visible in dev
    // eslint-disable-next-line no-console
    console.debug(
      '[analytics]',
      hasAnalyticsConsent() ? '' : '[consent-blocked]',
      resolvedName !== eventName ? `${eventName} → ${resolvedName}` : eventName,
      params,
    );
  }

  if (!hasAnalyticsConsent()) {
    return;
  }

  if (typeof window !== 'undefined' && typeof window.gtag === 'function') {
    window.gtag('event', resolvedName, params ? sanitizeForGa4(params) : params);
  }

  // PostHog has no GA4-style property name restrictions — send original params unchanged.
  forwardToPostHog(resolvedName, params);
}

/**
 * Unified product event tracker — fires to both GA4 and PostHog.
 * Use this for all product instrumentation events.
 */
export function trackProductEvent<
  T extends Record<string, string | number | boolean> = Record<string, string | number | boolean>,
>(
  eventName: string,
  params?: T,
): void {
  trackEvent(eventName, params);
}

/**
 * Typed event tracker — enforces the TypedAnalyticsEvent discriminated union.
 *
 * Use this for all events defined in the TypedAnalyticsEvent union. The
 * discriminated union provides compile-time validation that required properties
 * are present and correctly typed for each event name.
 *
 * Untyped or ad-hoc events that are not yet in TypedAnalyticsEvent should
 * continue using trackEvent() or trackProductEvent() during migration.
 *
 * @example
 *   trackTypedEvent({ event: RECOMMENDATION_APPLIED, properties: { site_id: '1', recommendation_id: '5', action_type: 'content_rewrite' } });
 */
export function trackTypedEvent(payload: TypedAnalyticsEvent): void {
  trackEvent(
    payload.event,
    payload.properties as unknown as Record<string, string | number | boolean>,
  );
}

/**
 * Canonical user trait schema version.
 *
 * Increment this constant whenever traits are added, removed, or renamed so
 * PostHog profile migrations can be tracked and historical schema diffs are
 * auditable without inspecting event payloads.
 *
 * Version history:
 *   v1 — initial: plan_tier, site_count, days_since_signup
 *   v2 — added: drafts_utilization_pct, sites_utilization_pct (optional)
 *   v3 — added: trait_schema_version sentinel property
 */
export const USER_TRAIT_SCHEMA_VERSION = 3;

/**
 * Identify the authenticated user in GA4 and PostHog for cross-session
 * user-level analysis.
 *
 * Sets `user_id` so GA4 links events to a backend user record.
 * Sets `user_properties` with the canonical trait set below so segments
 * can be built without joining to backend data.
 *
 * ## Canonical trait schema (v3)
 *
 * | Trait                   | Type    | Required | Description                                     |
 * |-------------------------|---------|----------|-------------------------------------------------|
 * | plan_tier               | string  | yes      | Subscription tier (e.g. "free", "pro", "team")  |
 * | site_count              | number  | yes      | Number of sites the user has created            |
 * | days_since_signup       | number  | yes      | Calendar days elapsed since account creation    |
 * | drafts_utilization_pct  | number  | no       | Draft limit used, 0–100 (omitted when unknown)  |
 * | sites_utilization_pct   | number  | no       | Site limit used, 0–100 (omitted when unknown)   |
 * | trait_schema_version    | number  | yes      | Schema version — always USER_TRAIT_SCHEMA_VERSION |
 *
 * When adding or removing traits, update USER_TRAIT_SCHEMA_VERSION and the
 * table above. Never remove a trait without a migration plan for existing
 * PostHog cohorts that may depend on it.
 *
 * No-op when gtag is unavailable (dev/test/SSR) or consent not granted.
 */
export function identifyUser(
  userId: number,
  properties: {
    plan_tier: string;
    site_count: number;
    days_since_signup: number;
    drafts_utilization_pct?: number;
    sites_utilization_pct?: number;
  },
): void {
  if (!hasAnalyticsConsent()) {
    return;
  }

  if (typeof window === 'undefined') {
    return;
  }

  // GA4: only when gtag is available (may be blocked by ad blockers)
  if (typeof window.gtag === 'function') {
    const gaId = (window as unknown as Record<string, unknown>).__GA_ID as string | undefined;

    if (gaId) {
      window.gtag('config', gaId, { user_id: String(userId) });
    }

    window.gtag('set', 'user_properties', {
      plan_tier: properties.plan_tier,
      site_count: properties.site_count,
      days_since_signup: properties.days_since_signup,
    });
  }

  // PostHog: independent of gtag availability — fires on every Inertia navigation
  // where auth.user is present (DashboardLayout useEffect), ensuring re-identification
  // after login without a full page reload.
  if (window.posthog?.identify) {
    const posthogProps: Record<string, string | number | boolean> = {
      plan_tier: properties.plan_tier,
      site_count: properties.site_count,
      days_since_signup: properties.days_since_signup,
      trait_schema_version: USER_TRAIT_SCHEMA_VERSION,
    };
    if (properties.drafts_utilization_pct !== undefined) {
      posthogProps.drafts_utilization_pct = properties.drafts_utilization_pct;
    }
    if (properties.sites_utilization_pct !== undefined) {
      posthogProps.sites_utilization_pct = properties.sites_utilization_pct;
    }
    window.posthog.identify(String(userId), posthogProps);
  }
}

/**
 * Track a filter or tab change in GA4 and PostHog.
 *
 * Use this on Recommendations, Opportunity Map, and any page with filters
 * so feature usage can be measured by filter type.
 */
export function trackFilterApplied(filterName: string, filterValue: string): void {
  trackProductEvent(FILTER_APPLIED, { filter_name: filterName, filter_value: filterValue });
}

/**
 * Track a GA4 Enhanced Ecommerce event with the required schema.
 *
 * GA4 Enhanced Ecommerce mandates currency, value, and an items array where each
 * item carries item_id and item_name. Flat params (item_id at the top level) are
 * silently ignored by GA4 ecommerce reports, so this function calls gtag directly
 * instead of routing through trackEvent (which only supports flat scalar params).
 *
 * PostHog receives a flattened representation because it has no native items-array
 * schema for ecommerce.
 */
export function trackEcommerceEvent(
  action: 'begin_checkout' | 'purchase',
  item: { item_id: string; item_name: string; price: number },
  value: number,
  currency: string = 'USD',
): void {
  if (isAnalyticsDebug() && import.meta.env.DEV) {
    // eslint-disable-next-line no-console
    console.debug('[analytics]', hasAnalyticsConsent() ? '' : '[consent-blocked]', action, {
      currency,
      value,
      items: [item],
    });
  }

  if (!hasAnalyticsConsent()) {
    return;
  }

  const ga4Params = {
    currency,
    value,
    items: [
      {
        item_id: item.item_id,
        item_name: item.item_name,
        price: item.price,
        quantity: 1,
      },
    ],
  };

  if (typeof window !== 'undefined' && typeof window.gtag === 'function') {
    // Cast required: gtag event overload types params as Gtag.EventParams (scalar-only),
    // but ecommerce events require an items array per the GA4 Enhanced Ecommerce spec.
    (window.gtag as (cmd: string, action: string, params: Record<string, unknown>) => void)(
      'event',
      action,
      ga4Params,
    );
  }

  // PostHog: flatten items into top-level props for compatibility with its property model.
  forwardToPostHog(action, {
    currency,
    value,
    item_id: item.item_id,
    item_name: item.item_name,
    price: item.price,
  });
}

/**
 * Verify analytics infrastructure is loaded. Logs warning after timeout if gtag unavailable.
 * Also checks consent status so blind spots in funnel measurement are visible.
 * Call once on app mount.
 */
export function checkAnalyticsHealth(timeoutMs: number = 5000): void {
  if (typeof window === 'undefined') return;

  // Check consent status — if declined, client-side tracking is intentionally disabled.
  // Critical funnel events (signup, gsc_connected, first_analysis, recommendation_applied,
  // purchase) are tracked server-side via AnalyticsEventService and are unaffected.
  if (!hasAnalyticsConsent()) {
    if (isAnalyticsDebug()) {
      console.warn(
        '[analytics] consent not granted — client-side GA4/PostHog tracking disabled.',
        'Server-side funnel events still fire via AnalyticsEventService.',
      );
    }
    return;
  }

  setTimeout(() => {
    if (typeof window.gtag !== 'function') {
      if (isAnalyticsDebug()) {
        console.warn('[analytics] gtag not loaded after', timeoutMs, 'ms');
      }
      // Attempt to notify PostHog if available (may also be blocked)
      forwardToPostHog('analytics_health_check_failed', {
        reason: 'gtag_unavailable',
        timeout_ms: timeoutMs,
      });
      // Server-side fallback: POST directly to our own API so the failure
      // is recorded even when the analytics SDK is blocked by an ad-blocker.
      // Uses native fetch — no dependency on gtag or PostHog.
      void fetch('/api/analytics-health', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          reason: 'gtag_unavailable',
          timeout_ms: timeoutMs,
          user_agent: navigator.userAgent.slice(0, 500),
        }),
        // Best-effort: fire-and-forget, don't retry on failure
        keepalive: true,
      }).catch(() => {
        // Silently ignore — if even the direct POST fails, there is nothing
        // more we can do from the client side.
      });
    }
  }, timeoutMs);
}

/**
 * Track a consent decision via a server-side POST that fires regardless of
 * the user's analytics consent state.
 *
 * This measures consent acceptance rate — the percentage of sessions where
 * analytics consent is granted — without itself requiring consent. Measuring
 * consent is a legitimate interest under GDPR Art. 6(1)(f). No PII is sent;
 * only categorical booleans and a decision type string.
 *
 * Use this instead of trackEvent('consent_decision', ...) so the call is
 * never silently dropped when the user declines analytics.
 */
export function trackConsentDecision(
  analyticsGranted: boolean,
  marketingGranted: boolean,
  decisionType: 'accept_all' | 'decline_all' | 'custom',
): void {
  if (typeof window === 'undefined') return;

  try {
    const csrfToken = (document.querySelector('meta[name="csrf-token"]') as HTMLMetaElement | null)
      ?.content;

    void fetch('/api/consent-decision', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Accept: 'application/json',
        ...(csrfToken ? { 'X-CSRF-TOKEN': csrfToken } : {}),
      },
      body: JSON.stringify({
        analytics_granted: analyticsGranted,
        marketing_granted: marketingGranted,
        decision_type: decisionType,
      }),
      // Fire-and-forget — ensures the POST completes even if the user
      // navigates away immediately after clicking Accept/Decline.
      keepalive: true,
    }).catch(() => {
      // Best-effort — consent decision tracking must never throw or
      // block the consent UI interaction.
    });
  } catch {
    // Defensive — must not throw regardless of environment or CSP policy.
  }
}

// PostHog JS tracker configuration — read once at module load time so that
// loadAnalytics() can be called from anywhere (app.tsx init AND consent banner).
const _posthogKey = import.meta.env.VITE_POSTHOG_API_KEY as string | undefined;
const _posthogHost =
  (import.meta.env.VITE_POSTHOG_HOST as string | undefined) ?? 'https://app.posthog.com';

/**
 * Initialize PostHog if the API key is configured and analytics consent has been granted.
 *
 * Safe to call multiple times — posthog.__loaded guard prevents re-initialization.
 * Call on app load (for users who already consented) AND from the cookie consent
 * banner's accept handler (to initialize within the same session on first consent).
 */
export function loadAnalytics(): void {
  if (!_posthogKey) return;
  if (!getCategoryConsent('analytics')) return;

  import('posthog-js')
    .then(({ default: posthog }) => {
      if (posthog.__loaded) return;

      posthog.init(_posthogKey!, {
        api_host: _posthogHost,
        capture_pageview: true,
        capture_pageleave: true,
        autocapture: false,
      });

      const pageEl = document.querySelector('[data-page]');
      if (pageEl) {
        try {
          const pageData = JSON.parse(pageEl.getAttribute('data-page') ?? '{}') as {
            props?: {
              auth?: {
                user?: {
                  id?: number;
                  email?: string;
                  name?: string;
                  days_since_signup?: number;
                  subscription?: { tier?: string } | null;
                };
              };
              sites?: unknown[];
              plan?: string;
              posthog_traits?: Record<string, unknown> | null;
            };
          };
          const authUser = pageData.props?.auth?.user;
          if (authUser?.id) {
            const serverTraits = pageData.props?.posthog_traits;
            posthog.identify(String(authUser.id), {
              plan_tier: pageData.props?.plan ?? 'free',
              site_count: pageData.props?.sites?.length ?? 0,
              days_since_signup: authUser.days_since_signup ?? 0,
              ...(serverTraits ?? {}),
              trait_schema_version: USER_TRAIT_SCHEMA_VERSION,
            });
          }
        } catch {
          // noop
        }
      }
    })
    .catch(() => {
      // noop — PostHog is optional
    });
}

/**
 * Check if analytics debug mode is enabled via VITE_ANALYTICS_DEBUG env var.
 */
function isAnalyticsDebug(): boolean {
  if (typeof import.meta === 'undefined') return false;
  return (import.meta.env?.VITE_ANALYTICS_DEBUG as string) === 'true';
}
