import {
  type ClientPerspective,
  type QueryParams,
  type QueryWithoutParams,
  validateApiPerspective,
} from '@sanity/client'
import {urlSearchParamPreviewPerspective} from '@sanity/preview-url-secret/constants'
import type {HydrogenSession} from '@shopify/hydrogen'

import type {SanityPreviewSession} from './preview/session'

/**
 * Create an SHA-256 hash as a hex string
 * @see https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest#converting_a_digest_to_a_hex_string
 */
export async function sha256(message: string): Promise<string> {
  // encode as UTF-8
  const messageBuffer = await new TextEncoder().encode(message)
  // hash the message
  const hashBuffer = await crypto.subtle.digest('SHA-256', messageBuffer)
  // convert bytes to hex string
  return Array.from(new Uint8Array(hashBuffer))
    .map((b) => b.toString(16).padStart(2, '0'))
    .join('')
}

/**
 * Hash query and its parameters for use as cache key.
 * NOTE: Oxygen deployment will break if the cache key is long or contains `\n`
 */
export function hashQuery(
  query: string,
  params: QueryParams | QueryWithoutParams,
): Promise<string> {
  let hash = query

  if (params) {
    hash += JSON.stringify(params)
  }

  return sha256(hash)
}

/**
 * Sanitizes and validates a perspective value.
 * Handles both string (comma-separated) and array formats.
 */
export function sanitizePerspective(perspective: unknown): Exclude<ClientPerspective, 'raw'> {
  let sanitizedPerspective =
    typeof perspective === 'string' && perspective.includes(',')
      ? perspective.split(',')
      : perspective

  // Filter out empty strings and undefined values from perspective array
  if (Array.isArray(sanitizedPerspective)) {
    sanitizedPerspective = sanitizedPerspective.filter(
      (p): p is string => typeof p === 'string' && p.length > 0,
    )
  }

  validateApiPerspective(sanitizedPerspective)

  return sanitizedPerspective === 'raw' ? 'drafts' : sanitizedPerspective
}

/**
 * Check if API version supports perspective stack (v2025-02-19 or later)
 * Special versions: '1' doesn't support perspectives, 'X' does support perspectives
 */
export function supportsPerspectiveStack(apiVersion: string): boolean {
  // Special cases
  if (apiVersion === '1') return false
  if (apiVersion === 'X') return true

  // Normalize version by removing 'v' prefix if present
  const normalizedVersion = `${apiVersion}`.replace(/^v/, '')

  // Parse date format: 2025-02-19
  if (!/^\d{4}-\d{2}-\d{2}$/.test(normalizedVersion)) return false

  const versionDate = new Date(normalizedVersion)
  const cutoffDate = new Date('2025-02-19')

  return versionDate >= cutoffDate
}

/**
 * Extracts and validates the perspective from a session.
 */
export function getPerspective(session: SanityPreviewSession | HydrogenSession): ClientPerspective {
  const perspective = session
    .get('perspective')
    ?.split(',')
    .filter((p: string) => p.length > 0)
  validateApiPerspective(perspective)
  return perspective
}

/**
 * Reads the `sanity-preview-perspective` URL search param and validates it.
 * Returns `undefined` if absent or invalid, so callers can fall back to the session.
 */
export function getPerspectiveFromUrl(url: URL | string): ClientPerspective | undefined {
  try {
    const parsed = typeof url === 'string' ? new URL(url) : url
    const param = parsed.searchParams.get(urlSearchParamPreviewPerspective)
    if (!param) return undefined
    return sanitizePerspective(param)
  } catch {
    return undefined
  }
}

/**
 * Type guard that checks if a session object is a SanityPreviewSession.
 * Validates presence of required methods: has, destroy (in addition to Hydrogen session methods).
 */
export function isSanityPreviewSession(session: unknown): session is SanityPreviewSession {
  return (
    isHydrogenSession(session) &&
    'has' in session &&
    typeof session.has === 'function' &&
    'destroy' in session &&
    typeof session.destroy === 'function'
  )
}

/**
 * Type guard that checks if a session object is a valid Hydrogen session.
 * Validates presence of required methods: get, set, unset, commit.
 */
export function isHydrogenSession(session: unknown): session is HydrogenSession {
  return (
    !!session &&
    typeof session === 'object' &&
    'get' in session &&
    typeof session.get === 'function' &&
    'set' in session &&
    typeof session.set === 'function' &&
    'unset' in session &&
    typeof session.unset === 'function' &&
    'commit' in session &&
    typeof session.commit === 'function'
  )
}

/**
 * Utility function that detects if code is running on the server.
 * Used for SSR safety and preventing client-only code from running on server.
 */
export function isServer(): boolean {
  return typeof document === 'undefined'
}
