/**
 * @file core.ts
 * @description Next.js-native, edge-compatible Sanity data fetcher with stega support
 * @author Invisible Cities Agency
 * @license MIT
 */

import { buildStegaConfig, processStegaResponse, type StegaConfig } from './stega';

// Helper to check if draft mode is enabled in Next.js
async function isDraftModeEnabled(): Promise<boolean> {
  try {
    // Dynamic import to avoid build issues in non-Next.js environments
    const { draftMode } = await import('next/headers');
    const draft = await draftMode();
    return draft.isEnabled;
  } catch {
    // Not in Next.js or draft mode not available
    return false;
  }
}

// Detect if current request should enable stega/visual editing overlays
// Uses Next.js headers/cookies if available, but degrades gracefully when not running in Next
export async function detectStegaRequest(options?: {
  /** Feature flag cookie name set by Studio or a toggle endpoint (default: 'ic_stega') */
  cookieName?: string;
  /** Optional custom header to allow one-shot enablement (default checks common names) */
  headerName?: string;
  /** Studio URL or origin to validate Referer against; defaults to NEXT_PUBLIC_SANITY_STUDIO_URL */
  studioUrl?: string;
  /** Force enable regardless of environment signals */
  forceEnable?: boolean;
  /** Force disable regardless of environment signals */
  forceDisable?: boolean;
}): Promise<boolean> {
  if (options?.forceEnable) return true;
  if (options?.forceDisable) return false;

  // Defaults
  const cookieName = options?.cookieName ?? 'ic_stega';
  const studioEnv = options?.studioUrl || process.env.NEXT_PUBLIC_SANITY_STUDIO_URL || process.env.SANITY_STUDIO_URL;

  try {
    // Dynamic import to avoid hard Next.js dependency
    const mod = await import('next/headers');
    const cookies = (mod as any).cookies?.bind(mod);
    const headers = (mod as any).headers?.bind(mod);

    const draft = (await ((mod as any).draftMode?.() ?? { isEnabled: false }));
    if (draft?.isEnabled) return true;

    // Check cookie flag
    const hasCookie = typeof cookies === 'function' ? Boolean(cookies().get(cookieName)?.value) : false;
    if (hasCookie) return true;

    // Check custom or common headers
    const hdrs = typeof headers === 'function' ? headers() : undefined;
    const headerName = options?.headerName;
    const headerCandidates = [headerName, 'x-ic-stega', 'x-sanity-present', 'x-sanity-preview'].filter(Boolean) as string[];
    if (hdrs) {
      for (const name of headerCandidates) {
        const v = hdrs.get(name);
        if (v && v !== '0' && v.toLowerCase() !== 'false') return true;
      }
    }

    // Referer origin check against Studio origin
    if (hdrs && studioEnv) {
      const ref = hdrs.get('referer');
      if (ref) {
        try {
          const refererOrigin = new URL(ref).origin;
          const studioOrigin = new URL(studioEnv).origin;
          if (refererOrigin === studioOrigin) return true;
        } catch {
          // ignore URL parse errors
        }
      }
    }
  } catch {
    // Not in Next.js environment; fall through
  }

  // Fallback: enable in development if explicitly configured via env
  if (process.env.NEXT_PUBLIC_ENABLE_STEGA === '1') return true;
  return false;
}

// Get config from environment variables
const getProjectId = () => {
  const id = process.env.NEXT_PUBLIC_SANITY_PROJECT_ID;
  if (!id) {
    throw new Error('NEXT_PUBLIC_SANITY_PROJECT_ID environment variable is required');
  }
  return id;
};

const apiVersion = process.env.NEXT_PUBLIC_SANITY_API_VERSION || '2025-02-10';

// Get the viewer token - check multiple possible env vars
const getViewerToken = () => {
  return process.env.SANITY_VIEWER_TOKEN || 
         process.env.SANITY_API_READ_TOKEN ||
         process.env.NEXT_PUBLIC_SANITY_VIEWER_TOKEN;
};

export type QueryParams = Record<string, string | number | boolean | null | undefined | Array<string | number | boolean>>;

export interface EdgeSanityFetchOptions {
  /** Sanity dataset to query (e.g., 'production', 'staging') */
  dataset: string;
  /** GROQ query string */
  query: string;
  /** Optional query parameters for GROQ placeholders */
  params?: QueryParams;
  /** Whether to use Sanity's CDN (faster but no auth) */
  useCdn?: boolean;
  /** Whether to include auth token for draft preview access */
  useAuth?: boolean;
  /** Stega configuration for visual editing */
  stega?: Partial<StegaConfig>;
}

/**
 * Simple rate limiter to prevent 429 errors
 * @internal
 */
class EdgeRateLimiter {
  private lastRequest = 0;
  private readonly minInterval = 100; // 10 req/sec max

  async throttle(): Promise<void> {
    const now = Date.now();
    const timeSinceLastRequest = now - this.lastRequest;

    if (timeSinceLastRequest < this.minInterval) {
      const delay = this.minInterval - timeSinceLastRequest;
      await new Promise(resolve => setTimeout(resolve, delay));
    }

    this.lastRequest = Date.now();
  }
}

const rateLimiter = new EdgeRateLimiter();

/**
 * Fetches data from Sanity using native fetch API
 * Compatible with Edge Runtime and static generation
 */
export async function edgeSanityFetch<T>({
  dataset,
  query,
  params = {},
  useCdn = false,
  useAuth = false,
  stega
}: EdgeSanityFetchOptions): Promise<T> {
  // Apply rate limiting
  await rateLimiter.throttle();

  // Build the query URL
  const projectId = getProjectId();

  // Determine if we need source maps for stega (before choosing base URL)
  const isDraftMode = useAuth;
  const stegaConfig = buildStegaConfig(isDraftMode, stega);

  // When stega is enabled, force non-CDN API to ensure resultSourceMap is returned
  const useCdnEffective = stegaConfig.enabled ? false : useCdn;
  const baseUrl = useCdnEffective
    ? `https://${projectId}.apicdn.sanity.io`
    : `https://${projectId}.api.sanity.io`;

  const url = new URL(`${baseUrl}/v${apiVersion}/data/query/${dataset}`);
  url.searchParams.set('query', query);
  
  if (useAuth) {
    // Use 'previewDrafts' perspective to see draft documents merged with published
    url.searchParams.set('perspective', 'previewDrafts');
  }
  
  // Request source maps when stega is enabled
  if (stegaConfig.enabled) {
    url.searchParams.set('resultSourceMap', 'true');
  }

  // Add parameters
  Object.entries(params).forEach(([key, value]) => {
    url.searchParams.set(`$${key}`, JSON.stringify(value));
  });

  // Build headers
  const headers: Record<string, string> = {
    'Accept': 'application/json',
  };

  // Use env var for auth to maintain static generation compatibility
  if (useAuth) {
    const envToken = getViewerToken();
    if (envToken) {
      headers['Authorization'] = `Bearer ${envToken}`;
    }
  }
  
  const response = await fetch(url.toString(), {
    method: 'GET',
    headers,
  });

  if (!response.ok) {
    await response.text(); // Consume the body to prevent memory leak
    throw new Error(`Sanity fetch failed: ${response.status} ${response.statusText}`);
  }

  const data = await response.json();
  
  // Process response with stega support (reuse stegaConfig from above)
  return processStegaResponse(data, isDraftMode, stegaConfig);
}

/**
 * Factory function to create a typed Sanity fetcher for a given dataset
 */
export function createEdgeSanityFetcher(dataset: string, useAuth = false, stega?: Partial<StegaConfig>) {
  return <T>(query: string, params?: QueryParams) => {
    const options: EdgeSanityFetchOptions = {
      dataset,
      query,
      useAuth,
      ...(stega !== undefined ? { stega } : {}),
      ...(params !== undefined ? { params } : {}),
    };
    return edgeSanityFetch<T>(options);
  };
}

/**
 * Next.js-aware Sanity fetcher that automatically handles draft mode
 * This is the primary fetcher for Next.js applications
 * 
 * @example
 * const data = await sanityFetch('*[_type == "post"][0]');
 */
export async function sanityFetch<T = unknown>(
  query: string,
  params?: QueryParams,
  options?: {
    dataset?: string;
    /** Override automatic draft mode detection */
    forceAuth?: boolean;
    /** Stega configuration for visual editing */
    stega?: Partial<StegaConfig>;
  }
): Promise<T> {
  const dataset = options?.dataset || process.env.NEXT_PUBLIC_SANITY_DATASET || 'production';
  const useAuth = options?.forceAuth ?? await isDraftModeEnabled();
  
  return edgeSanityFetch<T>({
    dataset,
    query,
    ...(params !== undefined ? { params } : {}),
    useCdn: !useAuth, // Use CDN when not authenticated
    useAuth,
    stega: options?.stega || { enabled: useAuth }, // Enable stega in draft mode by default
  });
}

/**
 * Presentation-aware hybrid fetcher
 * Automatically enables authenticated fetch + stega overlays when a Studio/Presentation
 * signal is detected (draftMode cookie, feature flag cookie, referer from Studio, or header).
 * Otherwise uses fast CDN fetch with no stega.
 */
export async function sanityFetchHybrid<T = unknown>(
  query: string,
  params?: QueryParams,
  options?: {
    dataset?: string;
    /** Optional stega options to merge with defaults */
    stega?: Partial<StegaConfig>;
    /** Detection overrides */
    cookieName?: string;
    headerName?: string;
    studioUrl?: string;
    forceEnableStega?: boolean;
    forceDisableStega?: boolean;
  }
): Promise<T> {
  const dataset = options?.dataset || process.env.NEXT_PUBLIC_SANITY_DATASET || 'production';
  const enableStega = await detectStegaRequest({
    ...(options?.cookieName !== undefined ? { cookieName: options.cookieName } : {}),
    ...(options?.headerName !== undefined ? { headerName: options.headerName } : {}),
    ...(options?.studioUrl !== undefined ? { studioUrl: options.studioUrl } : {}),
    ...(options?.forceEnableStega !== undefined ? { forceEnable: options.forceEnableStega } : {}),
    ...(options?.forceDisableStega !== undefined ? { forceDisable: options.forceDisableStega } : {}),
  });

  return edgeSanityFetch<T>({
    dataset,
    query,
    ...(params !== undefined ? { params } : {}),
    useCdn: !enableStega,
    useAuth: enableStega,
    stega: { enabled: enableStega, ...(options?.stega || {}) },
  });
}

/**
 * Sanity fetcher with automatic draft fallback
 * Tries to fetch published content first, falls back to drafts if empty
 * Perfect for singleton documents that might only exist as drafts
 * 
 * @example
 * const page = await sanityFetchWithFallback('*[_type == "page" && slug.current == $slug][0]', { slug });
 */
export async function sanityFetchWithFallback<T = unknown>(
  query: string,
  params?: QueryParams,
  options?: {
    dataset?: string;
    /** Log when falling back to drafts */
    logFallback?: boolean;
    /** Stega configuration for visual editing */
    stega?: Partial<StegaConfig>;
  }
): Promise<T> {
  const dataset = options?.dataset || process.env.NEXT_PUBLIC_SANITY_DATASET || 'production';
  const isNextDraftMode = await isDraftModeEnabled();
  
  // If already in draft mode, just use authenticated fetch
  if (isNextDraftMode) {
    return edgeSanityFetch<T>({
      dataset,
      query,
      ...(params !== undefined ? { params } : {}),
      useCdn: false,
      useAuth: true,
      stega: options?.stega || { enabled: true },
    });
  }
  
  // Try published content first
  const publishedResult = await edgeSanityFetch<T>({
    dataset,
    query,
    ...(params !== undefined ? { params } : {}),
    useCdn: true,
    useAuth: false,
  });
  
  // If we got content, return it
  if (publishedResult) {
    return publishedResult;
  }
  
  // No published content, try drafts
  if (options?.logFallback !== false && process.env.NODE_ENV !== 'production') {
    console.warn('[sanityFetchWithFallback] No published content found, checking for drafts...');
  }
  
  const draftResult = await edgeSanityFetch<T>({
    dataset,
    query,
    ...(params !== undefined ? { params } : {}),
    useCdn: false,
    useAuth: true,
    stega: options?.stega || { enabled: true },
  });
  
  if (draftResult && options?.logFallback !== false && process.env.NODE_ENV !== 'production') {
    console.warn('[sanityFetchWithFallback] Draft content found and returned');
  }
  
  return draftResult;
}

/**
 * Static content fetcher - always uses CDN, never authenticates
 * Use for global settings and content that rarely changes
 * 
 * @example
 * const settings = await sanityFetchStatic('*[_type == "siteSettings"][0]');
 */
export async function sanityFetchStatic<T = unknown>(
  query: string,
  params?: QueryParams,
  dataset?: string
): Promise<T> {
  return edgeSanityFetch<T>({
    dataset: dataset || process.env.NEXT_PUBLIC_SANITY_DATASET || 'production',
    query,
    ...(params !== undefined ? { params } : {}),
    useCdn: true,
    useAuth: false,
  });
}

/**
 * Authenticated fetcher - always uses authentication
 * Use when you need to ensure draft content is visible
 * 
 * @example
 * const drafts = await sanityFetchAuthenticated('*[_type == "post" && _id in path("drafts.**")]');
 */
export async function sanityFetchAuthenticated<T = unknown>(
  query: string,
  params?: QueryParams,
  dataset?: string
): Promise<T> {
  return edgeSanityFetch<T>({
    dataset: dataset || process.env.NEXT_PUBLIC_SANITY_DATASET || 'production',
    query,
    ...(params !== undefined ? { params } : {}),
    useCdn: false,
    useAuth: true,
  });
}