import type {
  CustomAppBridgeMode,
  SkyPhoneAppBridgeRequest,
  SkyPhoneAppBridgeResponse,
  SkyPhoneAppCapability,
} from '@/types/apps'
import type { NuiResponse } from '@/utils/nui'
import type { NotificationSoundId } from '@/utils/preferences'

const STORAGE_KEY_PATTERN = /^[A-Za-z0-9._-]{1,64}$/
const NOTIFICATION_SOUNDS: ReadonlySet<NotificationSoundId> = new Set([
  'chime',
  'signal',
  'soft',
])
const JSON_MAX_DEPTH = 8
const JSON_MAX_NODES = 512
const HANDLED_REQUEST_ID_LIMIT = 512
const STORAGE_VALUE_MAX_BYTES = 65_536
const DEEP_LINK_DATA_MAX_BYTES = 16_384

type JsonResult = { success: true; value: unknown } | { success: false }

export type CustomAppBridgeNotification = {
  appId: string
  route: string
  sound?: NotificationSoundId
  subtitle?: string
  text: string
  title: string
}

export type CustomAppBridgeTarget = {
  external: boolean
  id: string
  route: string
}

export type CustomAppBridgeEffect =
  | { type: 'close' }
  | { route: string; type: 'open' }

export type CustomAppBridgeResult = {
  effect?: CustomAppBridgeEffect
  response: SkyPhoneAppBridgeResponse
}

type CustomAppBridgeDependencies = {
  createNotification: (
    notification: CustomAppBridgeNotification,
  ) => string | null
  getSourceApp: () => { capabilities: readonly string[]; id: string }
  prepareExternalOpen: (appId: string, data?: unknown) => boolean
  resolveTarget: (appId: string) => CustomAppBridgeTarget | null
  storageCall: (
    endpoint: 'custom-app:storage:get' | 'custom-app:storage:set',
    payload: Record<string, unknown>,
  ) => Promise<NuiResponse>
}

function isRecord(value: unknown): value is Record<string, unknown> {
  return value !== null && typeof value === 'object' && !Array.isArray(value)
}

function hasOnlyKeys(
  value: Record<string, unknown>,
  allowedKeys: readonly string[],
): boolean {
  const allowed = new Set(allowedKeys)
  return Object.keys(value).every((key) => allowed.has(key))
}

function readText(value: unknown, maximumLength: number): string | null {
  if (typeof value !== 'string') return null
  const normalized = value.trim()
  return normalized.length > 0 && normalized.length <= maximumLength
    ? normalized
    : null
}

function cloneJsonValue(
  value: unknown,
  depth: number,
  state: { nodes: number; seen: WeakSet<object> },
): JsonResult {
  state.nodes += 1
  if (state.nodes > JSON_MAX_NODES || depth > JSON_MAX_DEPTH) {
    return { success: false }
  }
  if (
    value === null ||
    typeof value === 'boolean' ||
    typeof value === 'string'
  ) {
    return { success: true, value }
  }
  if (typeof value === 'number') {
    return Number.isFinite(value)
      ? { success: true, value }
      : { success: false }
  }
  if (typeof value !== 'object' || state.seen.has(value)) {
    return { success: false }
  }

  state.seen.add(value)
  if (Array.isArray(value)) {
    if (value.length > 128) return { success: false }
    const result: unknown[] = []
    for (const item of value) {
      const normalized = cloneJsonValue(item, depth + 1, state)
      if (!normalized.success) return normalized
      result.push(normalized.value)
    }
    state.seen.delete(value)
    return { success: true, value: result }
  }

  const prototype = Object.getPrototypeOf(value)
  if (prototype !== Object.prototype && prototype !== null) {
    return { success: false }
  }
  const entries = Object.entries(value)
  if (entries.length > 128) return { success: false }
  const result: Record<string, unknown> = {}
  for (const [key, item] of entries) {
    if (
      key.length < 1 ||
      key.length > 64 ||
      key === '__proto__' ||
      key === 'constructor' ||
      key === 'prototype'
    ) {
      return { success: false }
    }
    const normalized = cloneJsonValue(item, depth + 1, state)
    if (!normalized.success) return normalized
    result[key] = normalized.value
  }
  state.seen.delete(value)
  return { success: true, value: result }
}

function normalizeJsonValue(value: unknown, maximumBytes: number): JsonResult {
  const normalized = cloneJsonValue(value, 0, {
    nodes: 0,
    seen: new WeakSet(),
  })
  if (!normalized.success) return normalized

  const serialized = JSON.stringify(normalized.value)
  if (
    typeof serialized !== 'string' ||
    new TextEncoder().encode(serialized).byteLength > maximumBytes
  ) {
    return { success: false }
  }
  return normalized
}

function normalizeStorageGetPayload(payload: unknown): { key: string } | null {
  if (!isRecord(payload) || !hasOnlyKeys(payload, ['key'])) return null
  return typeof payload.key === 'string' &&
    STORAGE_KEY_PATTERN.test(payload.key)
    ? { key: payload.key }
    : null
}

function normalizeStorageSetPayload(
  payload: unknown,
): { key: string; revision: number; value: unknown } | null {
  if (
    !isRecord(payload) ||
    !hasOnlyKeys(payload, ['key', 'revision', 'value']) ||
    !Object.prototype.hasOwnProperty.call(payload, 'value') ||
    typeof payload.key !== 'string' ||
    !STORAGE_KEY_PATTERN.test(payload.key) ||
    typeof payload.revision !== 'number' ||
    !Number.isInteger(payload.revision) ||
    payload.revision < 0 ||
    payload.revision > 4_294_967_294
  ) {
    return null
  }

  const value = normalizeJsonValue(payload.value, STORAGE_VALUE_MAX_BYTES)
  return value.success
    ? { key: payload.key, revision: payload.revision, value: value.value }
    : null
}

function normalizeNotificationPayload(
  appId: string,
  payload: unknown,
): CustomAppBridgeNotification | null {
  if (
    !isRecord(payload) ||
    !hasOnlyKeys(payload, ['sound', 'subtitle', 'text', 'title'])
  ) {
    return null
  }

  const title = readText(payload.title, 80)
  const text = readText(payload.text, 240)
  if (!title || !text) return null

  const subtitle =
    payload.subtitle === undefined ? undefined : readText(payload.subtitle, 80)
  if (payload.subtitle !== undefined && !subtitle) return null
  const sound =
    payload.sound === undefined ||
    (typeof payload.sound === 'string' &&
      NOTIFICATION_SOUNDS.has(payload.sound as NotificationSoundId))
      ? (payload.sound as NotificationSoundId | undefined)
      : null
  if (sound === null) return null

  return {
    appId,
    route: `/apps/${appId}`,
    ...(sound ? { sound } : {}),
    ...(subtitle ? { subtitle } : {}),
    text,
    title,
  }
}

function normalizeOpenPayload(
  payload: unknown,
): { appId: string; data?: unknown } | null {
  if (
    !isRecord(payload) ||
    !hasOnlyKeys(payload, ['appId', 'data']) ||
    typeof payload.appId !== 'string' ||
    payload.appId.length < 1 ||
    payload.appId.length > 64
  ) {
    return null
  }
  if (!Object.prototype.hasOwnProperty.call(payload, 'data')) {
    return { appId: payload.appId }
  }

  const data = normalizeJsonValue(payload.data, DEEP_LINK_DATA_MAX_BYTES)
  if (!data.success || !isRecord(data.value)) return null
  return { appId: payload.appId, data: data.value }
}

function hasPermission(
  capabilities: readonly string[],
  permission: string,
): boolean {
  return capabilities.includes(permission)
}

function response(
  requestId: string,
  success: boolean,
  data?: unknown,
  error?: string,
): SkyPhoneAppBridgeResponse {
  return {
    ...(data === undefined ? {} : { data }),
    ...(error ? { error } : {}),
    requestId,
    success,
  }
}

export function getSkyPhoneAppCapabilities(
  permissions: readonly string[],
): SkyPhoneAppCapability[] {
  const capabilities: SkyPhoneAppCapability[] = []
  if (hasPermission(permissions, 'app.close')) capabilities.push('app.close')
  if (hasPermission(permissions, 'app.open')) capabilities.push('app.open')
  if (hasPermission(permissions, 'device.storage')) {
    capabilities.push('device.storage.get', 'device.storage.set')
  }
  if (hasPermission(permissions, 'locale.read'))
    capabilities.push('locale.read')
  if (hasPermission(permissions, 'notifications')) {
    capabilities.push('notification.create')
  }
  if (hasPermission(permissions, 'theme.read')) capabilities.push('theme.read')
  return capabilities
}

export function shouldReportCustomAppReady(
  bridgeMode: CustomAppBridgeMode,
  signal: 'bridge-ready' | 'frame-load',
): boolean {
  return bridgeMode === 'sky'
    ? signal === 'bridge-ready'
    : signal === 'frame-load'
}

export function getCustomAppFrameBootstrapMessages(
  compatibility: Readonly<Record<string, unknown>>,
): readonly unknown[] {
  if (compatibility.provider === 'lb_phone') {
    return ['componentsLoaded']
  }
  return []
}

export function createCustomAppBridgeRequestHandler(
  dependencies: CustomAppBridgeDependencies,
): {
  handle: (
    request: SkyPhoneAppBridgeRequest,
  ) => Promise<CustomAppBridgeResult | null>
} {
  const handledRequestIds = new Set<string>()
  const handledRequestOrder: string[] = []

  return {
    async handle(
      request: SkyPhoneAppBridgeRequest,
    ): Promise<CustomAppBridgeResult | null> {
      if (
        typeof request.requestId !== 'string' ||
        request.requestId.length < 1 ||
        request.requestId.length > 128 ||
        typeof request.method !== 'string' ||
        request.method.length < 1 ||
        request.method.length > 64 ||
        handledRequestIds.has(request.requestId)
      ) {
        return null
      }
      handledRequestIds.add(request.requestId)
      handledRequestOrder.push(request.requestId)
      if (handledRequestOrder.length > HANDLED_REQUEST_ID_LIMIT) {
        const expiredRequestId = handledRequestOrder.shift()
        if (expiredRequestId) handledRequestIds.delete(expiredRequestId)
      }

      const source = dependencies.getSourceApp()
      try {
        if (request.method === 'app.close') {
          return hasPermission(source.capabilities, 'app.close')
            ? {
                effect: { type: 'close' },
                response: response(request.requestId, true),
              }
            : {
                response: response(
                  request.requestId,
                  false,
                  undefined,
                  'permission_denied',
                ),
              }
        }

        if (
          request.method === 'device.storage.get' ||
          request.method === 'device.storage.set'
        ) {
          if (!hasPermission(source.capabilities, 'device.storage')) {
            return {
              response: response(
                request.requestId,
                false,
                undefined,
                'permission_denied',
              ),
            }
          }

          const storagePayload =
            request.method === 'device.storage.get'
              ? normalizeStorageGetPayload(request.payload)
              : normalizeStorageSetPayload(request.payload)
          if (!storagePayload) {
            return {
              response: response(
                request.requestId,
                false,
                undefined,
                'invalid_storage_request',
              ),
            }
          }

          const result = await dependencies.storageCall(
            request.method === 'device.storage.get'
              ? 'custom-app:storage:get'
              : 'custom-app:storage:set',
            { appId: source.id, ...storagePayload },
          )
          return {
            response: response(
              request.requestId,
              result.success,
              result.data,
              result.error,
            ),
          }
        }

        if (request.method === 'notification.create') {
          if (!hasPermission(source.capabilities, 'notifications')) {
            return {
              response: response(
                request.requestId,
                false,
                undefined,
                'permission_denied',
              ),
            }
          }
          const notification = normalizeNotificationPayload(
            source.id,
            request.payload,
          )
          if (!notification) {
            return {
              response: response(
                request.requestId,
                false,
                undefined,
                'invalid_notification',
              ),
            }
          }
          const notificationId = dependencies.createNotification(notification)
          return {
            response: response(request.requestId, true, { notificationId }),
          }
        }

        if (request.method === 'app.open') {
          if (!hasPermission(source.capabilities, 'app.open')) {
            return {
              response: response(
                request.requestId,
                false,
                undefined,
                'permission_denied',
              ),
            }
          }
          const openPayload = normalizeOpenPayload(request.payload)
          if (!openPayload) {
            return {
              response: response(
                request.requestId,
                false,
                undefined,
                'invalid_app_open',
              ),
            }
          }
          const target = dependencies.resolveTarget(openPayload.appId)
          if (!target) {
            return {
              response: response(
                request.requestId,
                false,
                undefined,
                'app_not_found',
              ),
            }
          }
          if (openPayload.data !== undefined && !target.external) {
            return {
              response: response(
                request.requestId,
                false,
                undefined,
                'open_data_not_supported',
              ),
            }
          }
          if (
            target.external &&
            !dependencies.prepareExternalOpen(target.id, openPayload.data)
          ) {
            return {
              response: response(
                request.requestId,
                false,
                undefined,
                'open_failed',
              ),
            }
          }
          return {
            effect: { route: target.route, type: 'open' },
            response: response(request.requestId, true, {
              appId: target.id,
            }),
          }
        }

        return {
          response: response(
            request.requestId,
            false,
            undefined,
            'unsupported_method',
          ),
        }
      } catch (error) {
        console.error(
          `[Custom apps] Bridge request failed for ${source.id}.`,
          error,
        )
        return {
          response: response(
            request.requestId,
            false,
            undefined,
            'request_failed',
          ),
        }
      }
    },
  }
}
