import type { CacheOptions, CacheService, CacheResult } from "@/types"
import type { ValkeyService } from "@/modules/cache/providers/valkey/valkeyTypes"

/**
 * Creates a cache service with the given options
 */
export const createCacheService = (
  cache: ValkeyService,
  options: CacheOptions = {},
): CacheService => {
  const prefix = options.prefix ? `${options.prefix}:` : "cache:"
  const defaultTtl = options.defaultTtl

  const buildKey = (key: string): string => `${prefix}${key}`

  const service: CacheService = {
    cache: async <T>(
      key: string,
      data: T,
      options: {
        ttl?: number
        serialize?: (data: T) => string
        deserialize?: (data: string) => T
      } = {},
    ): Promise<CacheResult<T>> => {
      const fullKey = buildKey(key)

      // Default serialization/deserialization uses JSON
      const serialize = options.serialize || JSON.stringify

      const serializedNewData = serialize(data)

      // Get previously cached data
      const previousData = await cache.get(fullKey)

      // Determine if data has changed
      const hasChanged =
        previousData === null || previousData !== serializedNewData

      // Cache the new data if it changed or doesn't exist
      if (hasChanged) {
        const ttl = typeof options?.ttl === 'number' ? options.ttl : defaultTtl

        if (ttl < 0) {
          await cache.set(fullKey, serializedNewData)
        } else {
          await cache.setex(fullKey, ttl, serializedNewData)
        }
      }

      return {
        changed: hasChanged,
        data: data,
      }
    },

    get: async <T>(
      key: string,
      deserialize: (data: string) => T = JSON.parse,
    ): Promise<T | null> => {
      const fullKey = buildKey(key)
      const data = await cache.get(fullKey)

      if (data === null) {
        return null
      }

      return deserialize(data)
    },

    forceUpdate: async <T>(
      key: string,
      data: T,
      options: { ttl?: number } = {},
    ): Promise<T> => {
      const fullKey = buildKey(key)
      const serialized = JSON.stringify(data)

      const ttl = options.ttl ?? defaultTtl

      if (ttl) {
        await cache.setex(fullKey, ttl, serialized)
      } else {
        await cache.set(fullKey, serialized)
      }

      return data
    },

    invalidate: async (key: string): Promise<void> => {
      const fullKey = buildKey(key)
      await cache.del(fullKey)
    },

    invalidatePattern: async (pattern: string): Promise<number> => {
      const fullPattern = buildKey(pattern)
      const keys = await cache.keys(fullPattern)

      if (keys.length > 0) {
        await cache.del(...keys)
      }

      return keys.length
    },

    // Get all keys matching a pattern (within this service's namespace)
    getKeysByPattern: async (pattern: string): Promise<string[]> => {
      const fullPattern = buildKey(pattern)
      const keys = await cache.keys(fullPattern)

      // Strip the prefix to return clean keys to the caller
      return keys.map((key) => key.replace(prefix, ""))
    },

    // Get TTL for a specific key
    getTtl: async (key: string): Promise<number> => {
      const fullKey = buildKey(key)
      return await cache.ttl(fullKey)
    },

    // Get multiple keys with their metadata
    getKeysWithMetadata: async (
      pattern: string,
    ): Promise<
      Array<{
        key: string
        ttl: number
        exists: boolean
      }>
    > => {
      const fullPattern = buildKey(pattern)
      const keys = await cache.keys(fullPattern)

      const metadata = await Promise.all(
        keys.map(async (fullKey) => {
          const cleanKey = fullKey.replace(prefix, "")
          const ttl = await cache.ttl(fullKey)

          return {
            key: cleanKey,
            ttl,
            exists: true,
          }
        }),
      )

      return metadata
    },

    // Invalidate multiple specific keys atomically
    invalidateMultiple: async (keys: string[]): Promise<number> => {
      if (keys.length === 0) return 0

      const fullKeys = keys.map((key) => buildKey(key))
      await cache.del(...fullKeys)

      return keys.length
    },

    // Check if a key exists
    exists: async (key: string): Promise<boolean> => {
      const fullKey = buildKey(key)
      const result = await cache.exists(fullKey)
      return result === 1
    },

    // Get key information (value + metadata)
    getWithMetadata: async <T>(
      key: string,
      deserialize: (data: string) => T = JSON.parse,
    ): Promise<{
      value: T | null
      ttl: number
      exists: boolean
    }> => {
      const fullKey = buildKey(key)

      const [value, ttl, exists] = await Promise.all([
        cache.get(fullKey),
        cache.ttl(fullKey),
        cache.exists(fullKey),
      ])

      return {
        value: value ? deserialize(value) : null,
        ttl,
        exists: exists === 1,
      }
    },

    namespace: (
      namespace: string,
      namespaceOptions: CacheOptions = {},
    ): CacheService => {
      const newPrefix = namespaceOptions.prefix || `${prefix}${namespace}:`
      const newOptions = {
        ...options,
        ...namespaceOptions,
        prefix: newPrefix,
      }

      return createCacheService(cache, newOptions)
    },
  }

  return service
}
