require "json"
require "./data_files"

module JosieHealth::LUT
  module CaffeineLUT
    # Serving sizes in ml
    SERVING_SIZES = {
      "can"     => 355,
      "cans"    => 355,
      "bottle"  => 500,
      "bottles" => 500,
      "cup"     => 240,
      "cups"    => 240,
      "mug"     => 350,
      "mugs"    => 350,
      "shot"    => 30,
      "shots"   => 30,
    }

    # Category defaults
    CATEGORY_DEFAULTS = {
      "energy_drink" => {caffeine_mg: 160, default_ml: 500},
      "energy_shot"  => {caffeine_mg: 200, default_ml: 60},
      "coffee"       => {caffeine_mg: 95, default_ml: 240},
      "tea"          => {caffeine_mg: 50, default_ml: 240},
      "soda"         => {caffeine_mg: 40, default_ml: 355},
    }

    @@brands : Hash(String, NamedTuple(caffeine_mg: Int32, category: String, default_ml: Int32))?
    @@powder_data : Hash(String, Int32)?
    @@alias_map : Hash(String, String)?
    @@loaded = false

    # Load caffeine data from embedded JSON
    def self.load_data
      return if @@loaded

      begin
        json = DataFiles.caffeine

        @@brands = {} of String => NamedTuple(caffeine_mg: Int32, category: String, default_ml: Int32)
        @@powder_data = {} of String => Int32
        @@alias_map = {} of String => String

        if brands_json = json["brands"]?
          brands_json.as_h.each do |name, data|
            canonical_name = name.to_s
            @@brands.not_nil![canonical_name] = {
              caffeine_mg: data["caffeine_mg"].as_i,
              category:    data["category"].as_s,
              default_ml:  data["default_ml"].as_i,
            }

            if mg_per_g = data["caffeine_mg_per_g"]?
              @@powder_data.not_nil![canonical_name] = mg_per_g.as_i
            end

            # Register aliases
            if aliases = data["aliases"]?
              aliases.as_a.each do |alias_name|
                @@alias_map.not_nil![alias_name.as_s] = canonical_name
              end
            end
          end
        end

        puts "Loaded #{@@brands.not_nil!.size} caffeine brands with #{@@alias_map.not_nil!.size} aliases"
        @@loaded = true
      rescue ex
        puts "Error loading caffeine.json: #{ex.message}"
        @@brands = {} of String => NamedTuple(caffeine_mg: Int32, category: String, default_ml: Int32)
        @@powder_data = {} of String => Int32
        @@alias_map = {} of String => String
        @@loaded = true
      end
    end

    # Resolve alias to canonical name
    def self.resolve_alias(name : String) : String
      load_data
      normalized = name.downcase.gsub(/[\s\-_]/, "")
      @@alias_map.not_nil![normalized]? || normalized
    end

    # Lookup a brand by name (handles aliases)
    def self.lookup_brand(name : String) : NamedTuple(caffeine_mg: Int32, category: String, default_ml: Int32)?
      load_data
      canonical = resolve_alias(name)
      @@brands.not_nil![canonical]?
    end

    # Check if a substance is a caffeinated beverage/product
    def self.is_caffeine_source?(name : String) : Bool
      lookup_brand(name) != nil
    end

    # Lookup caffeine mg per gram for powder sources (e.g., matcha)
    def self.lookup_powder_mg_per_g(name : String) : Int32?
      load_data
      canonical = resolve_alias(name)
      @@powder_data.not_nil![canonical]?
    end

    # Get category defaults
    def self.get_category_defaults(category : String) : NamedTuple(caffeine_mg: Int32, default_ml: Int32)?
      CATEGORY_DEFAULTS[category.downcase]?
    end

    # Calculate caffeine mg from volume and brand info
    def self.calculate_mg(volume_ml : Float64, caffeine_per_serving : Int32, serving_ml : Int32) : Float64
      ((volume_ml / serving_ml) * caffeine_per_serving).round(1)
    end

    # Parse dosage string like "2cans", "500ml", "1cup", "2g"
    # Returns {count, serving_type, volume_ml} or nil if not parseable
    # Note: for "g" serving, count is truncated to Int32; use the original dosage string for precise weight
    def self.parse_dosage(dosage : String) : NamedTuple(count: Int32, serving: String?, volume_ml: Int32)?
      normalized = dosage.downcase.strip

      # Try to match "Nunit" pattern (e.g., "2cans", "1cup", "500ml")
      if match = normalized.match(/^(\d+(?:\.\d+)?)(ml|g|can|cans|bottle|bottles|cup|cups|mug|mugs|shot|shots)?$/)
        count_str = match[1]
        serving = match[2]?

        # Handle decimal counts
        count_f = count_str.to_f
        count = count_f.to_i

        if serving == "ml"
          # Direct ml specification
          return {count: 1, serving: "ml", volume_ml: count_f.to_i}
        elsif serving == "g"
          # Gram specification for powder sources
          return {count: count_f.to_i, serving: "g", volume_ml: 0}
        elsif serving && SERVING_SIZES.has_key?(serving)
          volume_ml = SERVING_SIZES[serving] * count
          return {count: count, serving: serving, volume_ml: volume_ml}
        elsif serving.nil?
          # Just a number, assume count of default serving
          return {count: count, serving: nil, volume_ml: 0}
        end
      end

      nil
    end

    # Full caffeine dose calculation
    # Input: dosage string, substance name
    # Returns: {caffeine_mg, annotation} or nil
    def self.calculate_dose(dosage : String, substance : String) : NamedTuple(caffeine_mg: Float64, annotation: String)?
      parsed = parse_dosage(dosage)
      return nil unless parsed

      brand_info = lookup_brand(substance)
      return nil unless brand_info

      canonical = resolve_alias(substance)

      # Gram-based calculation for powder sources
      if parsed[:serving] == "g"
        mg_per_g = lookup_powder_mg_per_g(substance)
        return nil unless mg_per_g
        weight_g = dosage.rstrip("g").to_f
        caffeine_mg = (weight_g * mg_per_g).round(1)
        weight_str = weight_g == weight_g.to_i ? weight_g.to_i.to_s : weight_g.to_s
        details = "#{canonical} #{weight_str}g = #{caffeine_mg.round.to_i}mg"
        return {caffeine_mg: caffeine_mg, annotation: details}
      end

      # Determine volume
      volume_ml = if parsed[:volume_ml] > 0
                    parsed[:volume_ml]
                  else
                    parsed[:count] * brand_info[:default_ml]
                  end

      caffeine_mg = calculate_mg(volume_ml.to_f, brand_info[:caffeine_mg], brand_info[:default_ml])

      # Build details string
      serving_str = parsed[:serving] ? "#{parsed[:count]}#{parsed[:serving]}" : parsed[:count].to_s
      details = "#{canonical} #{serving_str} (#{volume_ml}ml) = #{caffeine_mg.to_i}mg"

      {caffeine_mg: caffeine_mg, annotation: details}
    end

    # Get all brand names
    def self.all_brands : Array(String)
      load_data
      @@brands.not_nil!.keys
    end
  end
end
