require "redis"
require "json"
require "time"
require "josie-health-utils"

class RedisClient
  Log                = JosieHealth::Utils::Log
  RETRY_ATTEMPTS     = 3
  RETRY_DELAY        = 500.milliseconds
  CONNECTION_TIMEOUT =  5
  READ_TIMEOUT       = 10

  # TTL constants
  VWRAPPED_JOB_TTL = 900 # 15 minutes - max time for video generation
  OAUTH_STATE_TTL  = 600 # 10 minutes - OAuth flow timeout

  @redis : Redis
  @redis_host : String
  @redis_port : Int32
  @redis_password : String?
  @connection_mutex : Mutex

  def initialize
    @redis_host = ENV["REDIS_HOST"]? || "localhost"
    @redis_port = (ENV["REDIS_PORT"]? || "6379").to_i
    @redis_password = ENV["REDIS_PASSWORD"]?
    @connection_mutex = Mutex.new

    @redis = Redis.new(
      host: @redis_host,
      port: @redis_port,
      password: @redis_password,
      connect_timeout: CONNECTION_TIMEOUT.seconds,
      command_timeout: READ_TIMEOUT.seconds
    )

    Log.debug("redis", "Client initialized - host: #{@redis_host}, port: #{@redis_port}")
  end

  private def connect_redis
    @redis = Redis.new(
      host: @redis_host,
      port: @redis_port,
      password: @redis_password,
      connect_timeout: CONNECTION_TIMEOUT.seconds,
      command_timeout: READ_TIMEOUT.seconds
    )
  end

  private def with_retry(&block)
    @connection_mutex.synchronize do
      with_retry_unsafe { yield }
    end
  end

  private def with_retry_unsafe(&block)
    attempts = 0
    loop do
      attempts += 1
      begin
        return yield
      rescue ex : Redis::ConnectionError | IO::Error
        if attempts < RETRY_ATTEMPTS
          Log.warn("redis", "Connection error (attempt #{attempts}/#{RETRY_ATTEMPTS}): #{ex.message}")
          sleep RETRY_DELAY
          begin
            @redis.close rescue nil
            connect_redis
          rescue conn_ex
            Log.warn("redis", "Failed to reconnect: #{conn_ex.message}")
          end
        else
          Log.error("redis", "Operation failed after #{RETRY_ATTEMPTS} attempts: #{ex.message}")
          raise ex
        end
      end
    end
  end

  # Validate user_id doesn't contain Redis pattern characters
  # Prevents command injection via patterns like user_id = "*"
  private def validate_user_id!(user_id : String)
    if user_id.matches?(/[\*\?\[\]\\]/)
      raise ArgumentError.new("Invalid user_id: contains pattern characters")
    end
    if user_id.empty?
      raise ArgumentError.new("Invalid user_id: empty string")
    end
  end

  # SCAN-based key iteration (non-blocking alternative to KEYS)
  # Returns all keys matching the pattern without blocking Redis
  private def scan_keys(pattern : String) : Array(String)
    keys = [] of String
    cursor = "0"

    loop do
      result = with_retry { @redis.command(["SCAN", cursor, "MATCH", pattern, "COUNT", "100"]) }

      case result
      when Array
        cursor = result[0].to_s
        if batch = result[1]
          keys.concat(redis_to_string_array(batch))
        end
      else
        break
      end

      break if cursor == "0"
    end

    keys
  end

  # Universal helper to convert Redis responses to array of strings
  private def redis_to_string_array(result) : Array(String)
    case result
    when Array(Redis::RedisValue)
      result.map(&.to_s)
    when Array
      result.map(&.to_s)
    when String
      # Single value - wrap in array
      result.empty? ? [] of String : [result]
    when Nil
      [] of String
    else
      # Fallback - convert to string and wrap
      [result.to_s]
    end
  end

  # Universal helper to convert Redis hash response to Hash(String, String)
  private def redis_to_string_hash(result) : Hash(String, String)
    hash = {} of String => String

    case result
    when Hash
      result.each { |k, v| hash[k.to_s] = v.to_s }
    when Array
      # HGETALL returns array of [key, value, key, value, ...]
      i = 0
      while i < result.size - 1
        hash[result[i].to_s] = result[i + 1].to_s
        i += 2
      end
    when String
      # Empty result from Redis when key doesn't exist - return empty hash
      Log.debug("redis", "Returned string (likely empty result)")
    when Nil
      # Empty hash
    else
      Log.warn("redis", "Unexpected hash result type: #{result.class}")
    end

    hash
  end

  # Store a dose entry in Redis
  def store_dose(timestamp : String, user : String, substance : String, amount : String, route : String, note_text : String = "", normalized_substance : String? = nil, normalized_route : String? = nil, categories : String? = nil, dose_classification : String? = nil, local_timestamp : String? = nil)
    with_retry do
      # Add unique suffix to prevent overwrites when multiple doses have same timestamp
      unique_suffix = Random::Secure.hex(4)
      dose_key = "dose:#{user}:#{timestamp}:#{unique_suffix}"
      dose_data = {
        "timestamp"            => timestamp,
        "local_timestamp"      => local_timestamp || timestamp,
        "user"                 => user,
        "substance"            => substance,
        "amount"               => amount,
        "route"                => route,
        "annotation"           => note_text,
        "normalized_substance" => normalized_substance || substance,
        "normalized_route"     => normalized_route || route,
        "categories"           => categories || "",
        "dose_classification"  => dose_classification || "",
      }

      # Store the individual dose
      @redis.hset(dose_key, dose_data)

      # Add to user's dose list for easy retrieval
      user_doses_key = "user_doses:#{user}"
      @redis.zadd(user_doses_key, Time.parse_rfc3339(timestamp).to_unix_f.to_i64, dose_key)

      # Add to global dose list
      @redis.zadd("all_doses", Time.parse_rfc3339(timestamp).to_unix_f.to_i64, dose_key)

      Log.debug("redis", "Stored dose: #{dose_key}")
    end
  end

  # Get all doses for a user in CSV format
  def fetch_csv_content : String
    @connection_mutex.synchronize do
      csv_lines = ["timestamp,user,substance,dosage,route"]

      # Get all doses ordered by timestamp - use command to get raw result
      dose_keys_result = with_retry_unsafe { @redis.command(["ZRANGE", "all_doses", "0", "-1"]) }

      # Convert to consistent array of strings
      dose_keys = redis_to_string_array(dose_keys_result)

      Log.debug("redis", "Fetched #{dose_keys.size} dose keys")

      dose_keys.each do |dose_key|
        # Get hash data for this dose - use original method for HGETALL
        dose_data_result = with_retry_unsafe { @redis.hgetall(dose_key) }

        # Convert to consistent hash
        dose_data = redis_to_string_hash(dose_data_result)

        if dose_data.size > 0
          line = "#{dose_data["timestamp"]},#{dose_data["user"]},#{dose_data["substance"]},#{dose_data["dosage"]? || dose_data["amount"]},#{dose_data["route"]}"
          csv_lines << line
        end
      end

      csv_content = csv_lines.join("\n")
      Log.debug("redis", "Generated CSV with #{csv_lines.size - 1} doses")

      csv_content
    end
  end

  # Get all dose keys from Redis
  def get_all_dose_keys : Array(String)
    dose_keys_result = with_retry { @redis.command(["ZRANGE", "all_doses", "0", "-1"]) }
    redis_to_string_array(dose_keys_result)
  end

  # Get dose data for a specific key
  def get_dose_data(dose_key : String) : Hash(String, String)
    dose_data_result = with_retry { @redis.hgetall(dose_key) }
    redis_to_string_hash(dose_data_result)
  end

  # Update dose data (for migrations)
  def update_dose_data(dose_key : String, data : Hash(String, String))
    with_retry do
      @redis.hset(dose_key, data)
    end
  end

  # Get doses for a specific user
  def get_user_doses(user : String, limit : Int32 = 100) : Array(Hash(String, String))
    user_doses_key = "user_doses:#{user}"

    # Get dose keys using command to avoid type issues
    dose_keys_result = with_retry { @redis.command(["ZREVRANGE", user_doses_key, "0", (limit - 1).to_s]) }

    # Convert to consistent array of strings
    dose_keys = redis_to_string_array(dose_keys_result)

    Log.debug("redis", "Fetched #{dose_keys.size} dose keys for user")

    doses = [] of Hash(String, String)
    dose_keys.each do |dose_key|
      # Get hash data for this dose - use original method for HGETALL
      dose_data_result = with_retry { @redis.hgetall(dose_key) }

      # Convert to consistent hash
      dose_data = redis_to_string_hash(dose_data_result)

      if dose_data.size > 0
        doses << dose_data
      end
    end

    doses
  end

  # Get recent doses for lastdose functionality
  def get_recent_doses(user : String, count : Int32 = 5) : Array(Hash(String, String))
    get_user_doses(user, count)
  end

  # Update annotation on the last dose for a user
  def update_last_dose_annotation(user : String, note_text : String) : Bool
    user_doses_key = "user_doses:#{user}"

    # Get the most recent dose key
    dose_keys_result = with_retry { @redis.command(["ZREVRANGE", user_doses_key, "0", "0"]) }
    dose_keys = redis_to_string_array(dose_keys_result)

    return false if dose_keys.empty?

    dose_key = dose_keys[0]

    # Update the annotation field
    with_retry do
      @redis.hset(dose_key, "annotation", note_text)
    end

    Log.debug("redis", "Updated annotation for #{dose_key}")
    true
  rescue
    false
  end

  # Health check
  def ping : Bool
    begin
      with_retry { @redis.ping == "PONG" }
    rescue
      false
    end
  end

  # Store vitals entry in Redis
  def store_vitals(timestamp : String, user : String, pulse : Int32?, systolic_bp : Int32?, diastolic_bp : Int32?, blood_oxygen : Int32?, temperature : Float64?, notes : String)
    vitals_key = "vitals:#{user}:#{timestamp}"
    with_retry do
      vitals_data = {
        "timestamp"    => timestamp,
        "user"         => user,
        "pulse"        => pulse.to_s,
        "systolic_bp"  => systolic_bp.to_s,
        "diastolic_bp" => diastolic_bp.to_s,
        "blood_oxygen" => blood_oxygen.to_s,
        "temperature"  => temperature.to_s,
        "notes"        => notes,
      }

      # Store the individual vitals entry
      @redis.hset(vitals_key, vitals_data)

      # Add to user's vitals list
      user_vitals_key = "user_vitals:#{user}"
      @redis.zadd(user_vitals_key, Time.parse_rfc3339(timestamp).to_unix_f.to_i64, vitals_key)

      # Add to global vitals list
      @redis.zadd("all_vitals", Time.parse_rfc3339(timestamp).to_unix_f.to_i64, vitals_key)

      Log.debug("redis", "Stored vitals: #{vitals_key}")
    end
  end

  # Store lab entry in Redis
  def store_lab(timestamp : String, user : String, test_type : String, value : String, unit : String, reference_range : String, notes : String)
    lab_key = "lab:#{user}:#{timestamp}"
    with_retry do
      lab_data = {
        "timestamp"       => timestamp,
        "user"            => user,
        "test_type"       => test_type,
        "value"           => value,
        "unit"            => unit,
        "reference_range" => reference_range,
        "notes"           => notes,
      }

      # Store the individual lab entry
      @redis.hset(lab_key, lab_data)

      # Add to user's labs list
      user_labs_key = "user_labs:#{user}"
      @redis.zadd(user_labs_key, Time.parse_rfc3339(timestamp).to_unix_f.to_i64, lab_key)

      # Add to global labs list
      @redis.zadd("all_labs", Time.parse_rfc3339(timestamp).to_unix_f.to_i64, lab_key)

      Log.debug("redis", "Stored lab: #{lab_key}")
    end
  end

  # Get vitals for a user
  def get_user_vitals(user : String, limit : Int32 = 100) : Array(Hash(String, String))
    user_vitals_key = "user_vitals:#{user}"

    # Get vitals keys using command to avoid type issues
    vitals_keys_result = with_retry { @redis.command(["ZREVRANGE", user_vitals_key, "0", (limit - 1).to_s]) }

    # Convert to consistent array of strings
    vitals_keys = redis_to_string_array(vitals_keys_result)

    vitals = [] of Hash(String, String)
    vitals_keys.each do |vitals_key|
      # Get hash data for these vitals
      vitals_data_result = with_retry { @redis.hgetall(vitals_key) }

      # Convert to consistent hash
      vitals_data = redis_to_string_hash(vitals_data_result)

      if vitals_data.size > 0
        vitals << vitals_data
      end
    end

    vitals
  end

  # Get labs for a user
  def get_user_labs(user : String, limit : Int32 = 100) : Array(Hash(String, String))
    user_labs_key = "user_labs:#{user}"

    # Get lab keys using command to avoid type issues
    lab_keys_result = with_retry { @redis.command(["ZREVRANGE", user_labs_key, "0", (limit - 1).to_s]) }

    # Convert to consistent array of strings
    lab_keys = redis_to_string_array(lab_keys_result)

    labs = [] of Hash(String, String)
    lab_keys.each do |lab_key|
      # Get hash data for this lab
      lab_data_result = with_retry { @redis.hgetall(lab_key) }

      # Convert to consistent hash
      lab_data = redis_to_string_hash(lab_data_result)

      if lab_data.size > 0
        labs << lab_data
      end
    end

    labs
  end

  # Remove last dose for undose functionality
  def remove_last_dose(user : String) : Bool
    with_retry do
      user_doses_key = "user_doses:#{user}"

      # Get the most recent dose key using command to avoid type issues
      recent_dose_keys_result = @redis.command(["ZREVRANGE", user_doses_key, "0", "0"])

      # Convert to consistent array of strings
      recent_dose_keys = redis_to_string_array(recent_dose_keys_result)

      return false if recent_dose_keys.empty?

      dose_key = recent_dose_keys[0]

      # Remove from user's dose list
      @redis.zrem(user_doses_key, dose_key)

      # Remove from global dose list
      @redis.zrem("all_doses", dose_key)

      # Delete the dose data
      @redis.del(dose_key)

      Log.debug("redis", "Removed dose: #{dose_key}")
      true
    end
  end

  # Delete a specific dose by timestamp
  def delete_dose(user : String, timestamp : String) : Bool
    with_retry do
      dose_key = find_dose_key(user, timestamp)
      return false unless dose_key

      user_doses_key = "user_doses:#{user}"

      # Remove from user's dose list
      @redis.zrem(user_doses_key, dose_key)

      # Remove from global dose list
      @redis.zrem("all_doses", dose_key)

      # Delete the dose data
      @redis.del(dose_key)

      Log.debug("redis", "Deleted dose: #{dose_key}")
      true
    end
  end

  # Update specific fields of a dose
  def update_dose(user : String, timestamp : String, fields : Hash(String, String)) : Bool
    with_retry do
      dose_key = find_dose_key(user, timestamp)
      return false unless dose_key

      # Update the fields
      @redis.hset(dose_key, fields)

      Log.debug("redis", "Updated dose: #{dose_key}")
      true
    end
  end

  # Get a specific dose by timestamp
  def get_dose(user : String, timestamp : String) : Hash(String, String)?
    with_retry do
      dose_key = find_dose_key(user, timestamp)
      return nil unless dose_key

      result = with_retry_unsafe { @redis.hgetall(dose_key) }
      data = redis_to_string_hash(result)
      data.empty? ? nil : data
    end
  end

  # Find dose key by user and timestamp (handles both old and new key formats)
  private def find_dose_key(user : String, timestamp : String) : String?
    # Try old format first (for backward compatibility)
    old_key = "dose:#{user}:#{timestamp}"
    if with_retry_unsafe { @redis.exists(old_key) } > 0
      return old_key
    end

    # Search in user's sorted set for keys matching the timestamp prefix
    user_doses_key = "user_doses:#{user}"
    dose_keys_result = with_retry_unsafe { @redis.command(["ZRANGE", user_doses_key, "0", "-1"]) }
    dose_keys = redis_to_string_array(dose_keys_result)

    prefix = "dose:#{user}:#{timestamp}:"
    dose_keys.find { |key| key.starts_with?(prefix) }
  end

  # Video job tracking
  def set_vwrapped_active(user : String, ttl_seconds : Int32 = VWRAPPED_JOB_TTL) : Bool
    # Set key with TTL (15 min default) - returns true if set (no existing job)
    key = "vwrapped:active:#{user}"
    with_retry do
      result = @redis.set(key, Time.utc.to_rfc3339, ex: ttl_seconds, nx: true)
      result != nil
    end
  end

  def is_vwrapped_active(user : String) : Bool
    key = "vwrapped:active:#{user}"
    with_retry do
      @redis.exists(key) > 0
    end
  end

  def clear_vwrapped_active(user : String) : Bool
    key = "vwrapped:active:#{user}"
    with_retry do
      @redis.del(key) > 0
    end
  end

  # ============================================
  # API Key Management
  # ============================================

  # Store a new API key (key_hash is SHA256 of the raw key)
  def store_api_key(key_hash : String, user_id : String, discord_id : String, name : String = "default") : Bool
    with_retry do
      key = "api_key:#{key_hash}"
      now = Time.utc.to_rfc3339

      key_data = {
        "user_id"    => user_id,
        "discord_id" => discord_id,
        "name"       => name,
        "created_at" => now,
        "last_used"  => now,
        "prefix"     => key_hash[0, 8],
      }

      @redis.hset(key, key_data)

      # Add to user's key list
      user_keys_key = "user_api_keys:#{user_id}"
      @redis.sadd(user_keys_key, key_hash)

      Log.debug("redis", "Stored API key (prefix: #{key_hash[0, 8]})")
      true
    end
  end

  # Get user_id from API key hash, returns nil if key not found
  def get_api_key_user(key_hash : String) : String?
    with_retry do
      key = "api_key:#{key_hash}"
      result = @redis.hget(key, "user_id")
      result.as?(String)
    end
  end

  # Get full API key data
  def get_api_key_data(key_hash : String) : Hash(String, String)?
    key = "api_key:#{key_hash}"
    result = with_retry { @redis.hgetall(key) }
    data = redis_to_string_hash(result)
    data.empty? ? nil : data
  end

  # List all keys for a user (returns metadata only, not the key itself)
  def list_user_keys(user_id : String) : Array(Hash(String, String))
    user_keys_key = "user_api_keys:#{user_id}"

    # Get all key hashes for this user
    key_hashes_result = with_retry { @redis.smembers(user_keys_key) }
    key_hashes = redis_to_string_array(key_hashes_result)

    keys = [] of Hash(String, String)
    key_hashes.each do |key_hash|
      if key_data = get_api_key_data(key_hash)
        # Return metadata only (prefix, name, created_at, last_used)
        keys << {
          "prefix"     => key_data["prefix"]? || key_hash[0, 8],
          "name"       => key_data["name"]? || "default",
          "created_at" => key_data["created_at"]? || "",
          "last_used"  => key_data["last_used"]? || "",
        }
      end
    end

    keys
  end

  # Revoke an API key by its hash
  def revoke_api_key(key_hash : String) : Bool
    with_retry do
      key = "api_key:#{key_hash}"

      # Get user_id first so we can remove from their list
      user_id = @redis.hget(key, "user_id").as?(String)

      if user_id
        # Remove from user's key list
        user_keys_key = "user_api_keys:#{user_id}"
        @redis.srem(user_keys_key, key_hash)
      end

      # Delete the key data
      deleted = @redis.del(key)
      Log.debug("redis", "Revoked API key (prefix: #{key_hash[0, 8]})")
      deleted > 0
    end
  end

  # Revoke an API key by prefix (first 8 chars of hash)
  def revoke_api_key_by_prefix(user_id : String, prefix : String) : Bool
    user_keys_key = "user_api_keys:#{user_id}"

    # Get all key hashes for this user
    key_hashes_result = with_retry { @redis.smembers(user_keys_key) }
    key_hashes = redis_to_string_array(key_hashes_result)

    # Find the key that matches the prefix
    key_hashes.each do |key_hash|
      if key_hash.starts_with?(prefix)
        return revoke_api_key(key_hash)
      end
    end

    false
  end

  # Update last_used timestamp for an API key
  def update_key_last_used(key_hash : String)
    with_retry do
      key = "api_key:#{key_hash}"
      @redis.hset(key, "last_used", Time.utc.to_rfc3339)
    end
  end

  # Count user's API keys (for rate limiting)
  def count_user_keys(user_id : String) : Int32
    user_keys_key = "user_api_keys:#{user_id}"
    with_retry do
      @redis.scard(user_keys_key).to_i32
    end
  end

  # ============================================
  # Discord User Mapping
  # ============================================

  # Store mapping from Discord ID to user_id
  def store_discord_mapping(discord_id : String, user_id : String)
    with_retry do
      @redis.set("discord_user:#{discord_id}", user_id)
      Log.debug("redis", "Stored Discord mapping")
    end
  end

  # Get user_id from Discord ID
  def get_user_by_discord_id(discord_id : String) : String?
    with_retry do
      result = @redis.get("discord_user:#{discord_id}")
      result.as?(String)
    end
  end

  # ============================================
  # OAuth State Management
  # ============================================

  # Store OAuth state token (for CSRF protection)
  def store_oauth_state(state : String, data : Hash(String, String), ttl_seconds : Int32 = OAUTH_STATE_TTL)
    with_retry do
      key = "oauth_state:#{state}"
      @redis.hset(key, data)
      @redis.expire(key, ttl_seconds)
    end
  end

  # Get and delete OAuth state (one-time use)
  def consume_oauth_state(state : String) : Hash(String, String)?
    key = "oauth_state:#{state}"
    result = with_retry { @redis.hgetall(key) }
    data = redis_to_string_hash(result)

    if data.empty?
      nil
    else
      with_retry { @redis.del(key) }
      data
    end
  end

  # ============================================
  # User Timezone Management
  # ============================================

  def get_user_timezone(user_id : String) : String?
    with_retry do
      result = @redis.get("tz:#{user_id}")
      result.as?(String)
    end
  end

  def set_user_timezone(user_id : String, timezone : String)
    with_retry do
      @redis.set("tz:#{user_id}", timezone)
    end
  end

  # ============================================
  # Alias Management
  # ============================================

  def set_alias(user_id : String, alias_name : String, doses_string : String)
    with_retry do
      @redis.set("alias:#{user_id}:#{alias_name}", doses_string)
    end
  end

  def get_alias(user_id : String, alias_name : String) : String?
    with_retry do
      result = @redis.get("alias:#{user_id}:#{alias_name}")
      result.as?(String)
    end
  end

  def remove_alias(user_id : String, alias_name : String) : Bool
    with_retry do
      @redis.del("alias:#{user_id}:#{alias_name}") > 0
    end
  end

  def get_user_aliases(user_id : String) : Hash(String, String)
    validate_user_id!(user_id)
    pattern = "alias:#{user_id}:*"
    alias_keys = scan_keys(pattern)

    aliases = {} of String => String
    alias_keys.each do |key|
      if alias_data = with_retry { @redis.get(key) }
        alias_name = key.sub("alias:#{user_id}:", "")
        aliases[alias_name] = alias_data.to_s
      end
    end
    aliases
  end

  # ============================================
  # Reminder Management
  # ============================================

  def store_reminder(user_id : String, message : String, trigger_time : Int64, channel_id : String, discord_user_id : String, callback_url : String, recurrence : String = "") : String
    reminder_id = "reminder:#{user_id}:#{Time.utc.to_unix_ms}"

    with_retry do
      reminder_data = {
        "user_id"         => user_id,
        "message"         => message,
        "trigger_time"    => trigger_time.to_s,
        "created_at"      => Time.utc.to_unix.to_s,
        "channel_id"      => channel_id,
        "discord_user_id" => discord_user_id,
        "callback_url"    => callback_url,
        "recurrence"      => recurrence,
      }

      @redis.hset(reminder_id, reminder_data)
      @redis.zadd("pending_reminders", trigger_time.to_f, reminder_id)
    end

    reminder_id
  end

  def get_due_reminders(current_time : Int64 = Time.utc.to_unix) : Array(Hash(String, String))
    reminder_ids_result = with_retry { @redis.command(["ZRANGEBYSCORE", "pending_reminders", "-inf", current_time.to_s]) }
    reminder_ids = redis_to_string_array(reminder_ids_result)

    reminders = [] of Hash(String, String)
    reminder_ids.each do |reminder_id|
      reminder_data_result = with_retry { @redis.hgetall(reminder_id) }
      reminder_data = redis_to_string_hash(reminder_data_result)
      if reminder_data.size > 0
        reminder_data["id"] = reminder_id
        reminders << reminder_data
      end
    end
    reminders
  end

  def get_user_reminders(user_id : String) : Array(Hash(String, String))
    validate_user_id!(user_id)
    pattern = "reminder:#{user_id}:*"
    reminder_keys = scan_keys(pattern)

    reminders = [] of Hash(String, String)
    reminder_keys.each do |key|
      reminder_data_result = with_retry { @redis.hgetall(key) }
      reminder_data = redis_to_string_hash(reminder_data_result)
      if reminder_data.size > 0
        reminder_data["id"] = key
        reminders << reminder_data
      end
    end
    reminders.sort_by { |r| r["trigger_time"]?.try(&.to_i64) || 0_i64 }
  end

  def remove_reminder(reminder_id : String) : Bool
    with_retry do
      @redis.del(reminder_id)
      @redis.zrem("pending_reminders", reminder_id)
      true
    end
  end

  def reschedule_reminder(reminder_id : String, new_trigger_time : Int64) : Bool
    with_retry do
      @redis.hset(reminder_id, "trigger_time", new_trigger_time.to_s)
      @redis.zadd("pending_reminders", new_trigger_time.to_f, reminder_id)
      true
    end
  end

  # ============================================
  # User Data Stats
  # ============================================

  # Get counts of all user data
  def get_user_data_stats(user_id : String) : Hash(String, Int32)
    validate_user_id!(user_id)
    counts = {
      "doses"     => 0,
      "vitals"    => 0,
      "labs"      => 0,
      "api_keys"  => 0,
      "aliases"   => 0,
      "reminders" => 0,
    }

    with_retry do
      # Count doses
      user_doses_key = "user_doses:#{user_id}"
      counts["doses"] = @redis.zcard(user_doses_key).to_i32

      # Count vitals
      user_vitals_key = "user_vitals:#{user_id}"
      counts["vitals"] = @redis.zcard(user_vitals_key).to_i32

      # Count labs
      user_labs_key = "user_labs:#{user_id}"
      counts["labs"] = @redis.zcard(user_labs_key).to_i32

      # Count API keys
      user_keys_key = "user_api_keys:#{user_id}"
      counts["api_keys"] = @redis.scard(user_keys_key).to_i32
    end

    # Count aliases (using SCAN outside with_retry to avoid nested mutex)
    alias_pattern = "alias:#{user_id}:*"
    counts["aliases"] = scan_keys(alias_pattern).size

    # Count reminders
    reminder_pattern = "reminder:#{user_id}:*"
    counts["reminders"] = scan_keys(reminder_pattern).size

    counts
  end

  # ============================================
  # User Data Deletion
  # ============================================

  # Delete all data for a user - returns count of deleted items
  def delete_all_user_data(user_id : String) : Hash(String, Int32)
    validate_user_id!(user_id)
    counts = {
      "doses"     => 0,
      "vitals"    => 0,
      "labs"      => 0,
      "api_keys"  => 0,
      "aliases"   => 0,
      "reminders" => 0,
      "settings"  => 0,
    }

    with_retry do
      # Delete doses
      user_doses_key = "user_doses:#{user_id}"
      dose_keys_result = @redis.command(["ZRANGE", user_doses_key, "0", "-1"])
      dose_keys = redis_to_string_array(dose_keys_result)
      dose_keys.each do |dose_key|
        @redis.zrem("all_doses", dose_key)
        @redis.del(dose_key)
        counts["doses"] += 1
      end
      @redis.del(user_doses_key)

      # Delete vitals
      user_vitals_key = "user_vitals:#{user_id}"
      vitals_keys_result = @redis.command(["ZRANGE", user_vitals_key, "0", "-1"])
      vitals_keys = redis_to_string_array(vitals_keys_result)
      vitals_keys.each do |vitals_key|
        @redis.zrem("all_vitals", vitals_key)
        @redis.del(vitals_key)
        counts["vitals"] += 1
      end
      @redis.del(user_vitals_key)

      # Delete labs
      user_labs_key = "user_labs:#{user_id}"
      labs_keys_result = @redis.command(["ZRANGE", user_labs_key, "0", "-1"])
      labs_keys = redis_to_string_array(labs_keys_result)
      labs_keys.each do |labs_key|
        @redis.zrem("all_labs", labs_key)
        @redis.del(labs_key)
        counts["labs"] += 1
      end
      @redis.del(user_labs_key)

      # Delete API keys
      user_keys_key = "user_api_keys:#{user_id}"
      key_hashes_result = @redis.smembers(user_keys_key)
      key_hashes = redis_to_string_array(key_hashes_result)
      key_hashes.each do |key_hash|
        @redis.del("api_key:#{key_hash}")
        counts["api_keys"] += 1
      end
      @redis.del(user_keys_key)

      # Delete user settings (timezone, etc)
      if @redis.del("tz:#{user_id}") > 0
        counts["settings"] += 1
      end
    end

    # Delete aliases (using SCAN outside with_retry to avoid nested mutex)
    alias_pattern = "alias:#{user_id}:*"
    alias_keys = scan_keys(alias_pattern)
    alias_keys.each do |alias_key|
      with_retry { @redis.del(alias_key) }
      counts["aliases"] += 1
    end

    # Delete reminders
    reminder_pattern = "reminder:#{user_id}:*"
    reminder_keys = scan_keys(reminder_pattern)
    reminder_keys.each do |reminder_key|
      with_retry do
        @redis.zrem("pending_reminders", reminder_key)
        @redis.del(reminder_key)
      end
      counts["reminders"] += 1
    end

    Log.info("redis", "Deleted all data for user #{user_id}: #{counts}")
    counts
  end

  # IRC Authentication

  def store_irc_auth_request(token : String, data : Hash(String, String))
    key = "irc_auth:#{token}"
    data.each do |field, value|
      @redis.hset(key, field, value)
    end
    @redis.expire(key, 600) # 10 minutes
  end

  def get_irc_auth_request(token : String) : Hash(String, String)?
    key = "irc_auth:#{token}"
    hash = @redis.hgetall(key)
    return nil if hash.empty?
    hash
  end

  def mark_irc_auth_verified(token : String, nick : String, account_name : String)
    key = "irc_auth:#{token}"
    return unless @redis.exists(key) == 1

    @redis.hset(key, "verified", "true")
    @redis.hset(key, "nick", nick)
    @redis.hset(key, "account_name", account_name)
    @redis.hset(key, "verified_at", Time.utc.to_rfc3339)
    @redis.expire(key, 600) # refresh TTL
  end

  def consume_irc_auth(token : String) : Hash(String, String)?
    key = "irc_auth:#{token}"
    hash = @redis.hgetall(key)
    return nil if hash.empty?
    return nil unless hash["verified"]? == "true"

    @redis.del(key)
    hash
  end

  def store_irc_user_mapping(network : String, nick : String, user_id : String)
    key = "irc_user:#{network.downcase}:#{nick.downcase}"
    @redis.set(key, user_id)
  end

  def get_user_by_irc(network : String, nick : String) : String?
    key = "irc_user:#{network.downcase}:#{nick.downcase}"
    @redis.get(key)
  end

  def get_all_user_dose_keys(user : String) : Array(String)
    user_doses_key = "user_doses:#{user}"
    dose_keys_result = with_retry { @redis.command(["ZRANGE", user_doses_key, "0", "-1"]) }
    redis_to_string_array(dose_keys_result)
  end

  def migrate_doses(from_user : String, to_user : String) : Int32
    dose_keys = get_all_user_dose_keys(from_user)
    return 0 if dose_keys.empty?

    migrated = 0
    to_user_doses_key = "user_doses:#{to_user}"
    from_user_doses_key = "user_doses:#{from_user}"

    dose_keys.each do |old_key|
      dose_data_result = with_retry { @redis.hgetall(old_key) }
      dose_data = redis_to_string_hash(dose_data_result)
      next if dose_data.empty?

      timestamp = dose_data["timestamp"]? || ""
      next if timestamp.empty?

      unique_suffix = Random::Secure.hex(4)
      new_key = "dose:#{to_user}:#{timestamp}:#{unique_suffix}"
      dose_data["user"] = to_user

      with_retry do
        @redis.hset(new_key, dose_data)
        @redis.zadd(to_user_doses_key, Time.parse_rfc3339(timestamp).to_unix_f, new_key)
        @redis.del(old_key)
        @redis.zrem(from_user_doses_key, old_key)
      end
      migrated += 1
    end

    Log.info("redis", "Migrated #{migrated} doses from #{from_user} to #{to_user}")
    migrated
  rescue ex
    Log.error("redis", "Migration error: #{ex.message}")
    0
  end

  def link_accounts(primary_user : String, secondary_user : String, method : String)
    key = "linked_accounts:#{primary_user}"
    with_retry do
      @redis.sadd(key, secondary_user)
      @redis.hset("account_link:#{secondary_user}", {
        "primary"   => primary_user,
        "linked_at" => Time.utc.to_rfc3339,
        "method"    => method,
      })
    end
  end

  def get_linked_accounts(user : String) : Array(String)
    key = "linked_accounts:#{user}"
    result = with_retry { @redis.smembers(key) }
    redis_to_string_array(result)
  end

  def get_primary_account(user : String) : String?
    key = "account_link:#{user}"
    result = with_retry { @redis.hget(key, "primary") }
    result.as?(String)
  end

  # Close connection
  def close
    @redis.close
  end
end
