// @ts-nocheck
import type Usercentrics from '@usercentrics/cmp-browser-sdk'
import type { BaseCategory, UserDecision } from '@usercentrics/cmp-browser-sdk'
import { proxy, snapshot, useSnapshot } from 'valtio'

import { IS_PLATFORM, LOCAL_STORAGE_KEYS } from './constants'

export type PriorConsentDecision =
  | null
  | { kind: 'uniform-accept' }
  | { kind: 'decisions'; decisions: UserDecision[] }

type UcDataServiceEntry = [string, { consent: boolean }]

const isValidUcDataServiceEntry = (entry: [string, unknown]): entry is UcDataServiceEntry => {
  const value = entry[1]
  return (
    typeof value === 'object' &&
    value !== null &&
    typeof (value as { consent: unknown }).consent === 'boolean'
  )
}

type UcSettingsService = { id: string; status: boolean }

const isValidUcSettingsService = (service: unknown): service is UcSettingsService =>
  typeof service === 'object' &&
  service !== null &&
  typeof (service as { id: unknown }).id === 'string' &&
  typeof (service as { status: unknown }).status === 'boolean'

/**
 * Check whether the user previously made a consent decision by reading
 * localStorage state that was written before UC.init() overwrites it.
 *
 * Returns enough information for the caller to restore the user's exact
 * prior state via UC.updateServices, or to fast-path via UC.acceptAllServices
 * when we can safely identify a uniform accept. Returns null when nothing
 * trustworthy can be detected — in that case the caller should show the
 * banner rather than fabricate a decision.
 *
 * Handles two scenarios (FE-2648 and GROWTH-790):
 *
 * 1. Slow navigation: GTM's Usercentrics integration replaced uc_settings with
 *    compressed ucString/ucData after the user's decision. On the next page
 *    load, UC.init() can't read that format and treats the user as new. We
 *    read ucData.consent.services directly and return every per-service
 *    decision so the caller can restore the user's exact state — including
 *    mixed states (essentials + functional accepted, tracking denied) that
 *    users produce via the Privacy Settings modal or the Opt out button.
 *
 * 2. Fast navigation: User decided on app A and navigated to app B before GTM
 *    finished writing ucData (or before GTM loaded at all, which happens on
 *    deny since TelemetryTagManager is gated behind hasAccepted). App B's
 *    UC.init() overwrites uc_settings with a fresh controllerId and resets
 *    uc_user_interaction to false. We check uc_user_interaction: "true" as
 *    a gate confirming the user actually interacted, then parse uc_settings
 *    to extract per-service decisions. uc_user_interaction alone is not
 *    enough — without uc_settings we cannot tell accept from deny and must
 *    not fabricate a direction.
 *
 * Both scenarios fail closed: any schema mismatch or partial corruption
 * returns null rather than proceeding with a subset of valid entries.
 * Over-consenting from malformed storage is the worst-direction bias in
 * this domain.
 *
 * Must be called BEFORE UC.init() since init overwrites these keys.
 */
export function detectPriorConsent(): PriorConsentDecision {
  try {
    const ucData = localStorage?.getItem('ucData')
    if (ucData) {
      const data = JSON.parse(ucData)
      const services = data?.consent?.services
      if (services && typeof services === 'object') {
        const rawEntries = Object.entries(services) as Array<[string, unknown]>
        if (rawEntries.length > 0) {
          if (!rawEntries.every(isValidUcDataServiceEntry)) {
            // Partial corruption: don't cherry-pick the valid subset. Fall
            // through to scenario 2 — uc_settings may still be intact.
          } else {
            const entries = rawEntries as UcDataServiceEntry[]
            if (entries.every(([, s]) => s.consent === true)) {
              return { kind: 'uniform-accept' }
            }
            return {
              kind: 'decisions',
              decisions: entries.map(([serviceId, s]) => ({
                serviceId,
                status: s.consent,
              })),
            }
          }
        }
      }
    }

    // uc_user_interaction gates trust in uc_settings — the SDK sets it on any
    // user interaction, which confirms uc_settings holds real decisions rather
    // than ruleset defaults.
    if (localStorage?.getItem('uc_user_interaction') === 'true') {
      const ucSettings = localStorage?.getItem('uc_settings')
      if (ucSettings) {
        const parsed = JSON.parse(ucSettings)
        const services = parsed?.services
        if (
          Array.isArray(services) &&
          services.length > 0 &&
          services.every(isValidUcSettingsService)
        ) {
          const decisions: UserDecision[] = services.map((s) => ({
            serviceId: s.id,
            status: s.status,
          }))
          if (decisions.every((d) => d.status === true)) {
            return { kind: 'uniform-accept' }
          }
          return { kind: 'decisions', decisions }
        }
      }
      // Flag says interacted but uc_settings is missing or malformed.
      // Don't fabricate direction — show the banner on the next init.
    }

    return null
  } catch {
    return null
  }
}

export const consentState = proxy({
  UC: null as Usercentrics | null,
  categories: null as BaseCategory[] | null,

  showConsentToast: false,
  hasConsented: false,
  acceptAll: () => {
    if (!consentState.UC) return
    const previousConsentValue = consentState.hasConsented

    consentState.hasConsented = true
    consentState.showConsentToast = false

    consentState.UC.acceptAllServices()
      .then(() => {
        consentState.categories = consentState.UC?.getCategoriesBaseInfo() ?? null
      })
      .catch(() => {
        consentState.hasConsented = previousConsentValue
        consentState.showConsentToast = true
      })
  },
  denyAll: () => {
    if (!consentState.UC) return
    const previousConsentValue = consentState.hasConsented

    consentState.hasConsented = false
    consentState.showConsentToast = false

    consentState.UC.denyAllServices()
      .then(() => {
        consentState.categories = consentState.UC?.getCategoriesBaseInfo() ?? null
      })
      .catch(() => {
        consentState.showConsentToast = previousConsentValue
      })
  },
  updateServices: (decisions: UserDecision[]) => {
    if (!consentState.UC) return

    consentState.showConsentToast = false

    consentState.UC.updateServices(decisions)
      .then(() => {
        consentState.hasConsented = consentState.UC?.areAllConsentsAccepted() ?? false
        consentState.categories = consentState.UC?.getCategoriesBaseInfo() ?? null
      })
      .catch(() => {
        consentState.showConsentToast = true
      })
  },
})

/**
 * Apply a prior consent decision (or lack of one) to the freshly-initialized
 * Usercentrics SDK and the module's consentState proxy. Extracted from
 * initUserCentrics to make the orchestration unit-testable without mocking
 * the dynamic SDK import. Must be called after UC.init(). Mutates
 * consentState synchronously and may call UC methods asynchronously.
 */
export function applyPriorDecisionToSDK(
  UC: Usercentrics,
  initialUIValues: { initialLayer: number },
  priorDecision: PriorConsentDecision
): void {
  consentState.UC = UC
  const hasConsented = UC.areAllConsentsAccepted()

  // If the SDK wants to show the banner but the user previously made a
  // decision (detected via ucData or uc_settings before init overwrote
  // them), silently re-apply that decision instead of re-prompting
  // (FE-2648, GROWTH-790).
  if (initialUIValues.initialLayer === 0 && !hasConsented && priorDecision) {
    consentState.categories = UC.getCategoriesBaseInfo()
    consentState.showConsentToast = false
    localStorage?.removeItem(LOCAL_STORAGE_KEYS.TELEMETRY_CONSENT)

    if (priorDecision.kind === 'uniform-accept') {
      // Uniform accept covers any currently-active service by definition,
      // including any added to the ruleset since the user's decision was
      // stored — acceptAllServices applies to all current services.
      consentState.hasConsented = true
      UC.acceptAllServices()
        .then(() => {
          consentState.categories = UC.getCategoriesBaseInfo()
        })
        .catch(() => {
          consentState.hasConsented = false
          consentState.showConsentToast = true
        })
      return
    }

    // priorDecision.kind === 'decisions'. Only suppress the banner if the
    // stored decisions cover every non-essential service the SDK currently
    // knows about. If the ruleset has grown since the user's ucData/
    // uc_settings was written, force a re-prompt rather than silently
    // defaulting the new service. Essentials are skipped because the SDK
    // forces them on regardless of user decision.
    const currentNonEssentialIds = UC.getServicesBaseInfo()
      .filter((s) => !s.isEssential)
      .map((s) => s.id)
    const coveredIds = new Set(priorDecision.decisions.map((d) => d.serviceId))
    const allCovered = currentNonEssentialIds.every((id) => coveredIds.has(id))

    if (!allCovered) {
      // Fall through to the banner path below. Reset the early writes
      // so the default-branch state assignments take effect correctly.
      consentState.showConsentToast = initialUIValues.initialLayer === 0
      consentState.hasConsented = hasConsented
      return
    }

    // Restore the user's exact per-service state — handles deny and any
    // partial/category-level decision made via Privacy Settings. hasConsented
    // is computed from SDK state after the restore resolves (will be false
    // unless every service was accepted).
    UC.updateServices(priorDecision.decisions)
      .then(() => {
        consentState.hasConsented = UC.areAllConsentsAccepted()
        consentState.categories = UC.getCategoriesBaseInfo()
      })
      .catch(() => {
        // Falling back to the banner is safer than silently flipping to a
        // uniform state the user didn't choose.
        consentState.showConsentToast = true
      })
    return
  }

  // 0 = first layer, aka show consent toast
  consentState.showConsentToast = initialUIValues.initialLayer === 0
  consentState.hasConsented = hasConsented
  consentState.categories = UC.getCategoriesBaseInfo()

  // If the user has previously consented (before usercentrics), accept all services
  if (!hasConsented && localStorage?.getItem(LOCAL_STORAGE_KEYS.TELEMETRY_CONSENT) === 'true') {
    consentState.acceptAll()
    localStorage.removeItem(LOCAL_STORAGE_KEYS.TELEMETRY_CONSENT)
  }
}

async function initUserCentrics() {
  if (process.env.NODE_ENV === 'test' || !IS_PLATFORM) return

  // [Alaister] For local development and staging, we accept all consent by default.
  // If you need to test usercentrics in these environments, comment out this
  // NEXT_PUBLIC_ENVIRONMENT check and add an ngrok domain to usercentrics
  if (
    process.env.NEXT_PUBLIC_ENVIRONMENT === 'local' ||
    process.env.NEXT_PUBLIC_ENVIRONMENT === 'staging'
  ) {
    consentState.hasConsented = true
    return
  }

  // Check for prior consent BEFORE UC.init(), which can't read the compressed
  // ucData format written by the GTM/Usercentrics integration (FE-2648).
  const priorDecision = detectPriorConsent()

  try {
    const { default: Usercentrics } = await import('@usercentrics/cmp-browser-sdk')

    const UC = new Usercentrics(process.env.NEXT_PUBLIC_USERCENTRICS_RULESET_ID!, {
      rulesetId: process.env.NEXT_PUBLIC_USERCENTRICS_RULESET_ID,
      useRulesetId: true,
    })

    const initialUIValues = await UC.init()
    applyPriorDecisionToSDK(UC, initialUIValues, priorDecision)
  } catch (error) {
    console.error('Failed to initialize Usercentrics:', error)
    // If SDK fails but user previously accepted uniformly, honor that.
    // For explicit per-service decisions we can't restore without the SDK,
    // and showing the banner when the SDK is broken would fail anyway.
    if (priorDecision?.kind === 'uniform-accept') {
      consentState.hasConsented = true
    }
  }
}

// Usercentrics is not available on the server
if (typeof window !== 'undefined') {
  initUserCentrics()
}

export function hasConsented() {
  return snapshot(consentState).hasConsented
}

export function useConsentState() {
  const snap = useSnapshot(consentState)

  return {
    hasAccepted: snap.hasConsented,
    categories: snap.categories as BaseCategory[] | null,
    acceptAll: snap.acceptAll,
    denyAll: snap.denyAll,
    updateServices: snap.updateServices,
  }
}
