local S = eye_spy.S

eye_spy.enrichers = {
  registry = {},
}

local node_tool_state_by_player = {}

local function add_line(view_model, id, text, color)
  view_model.lines[#view_model.lines + 1] = {
    id = id,
    text = text,
    color = color,
  }
end

local function meta_bool(meta, key, default_value)
  local raw = meta:get_string(key)

  if raw == "" then
    return default_value == true
  end

  return raw ~= "false"
end

local function growth_bar(percent)
  local slots = 10
  local p = math.max(0, math.min(100, percent or 0))
  local filled = math.floor((p / 100) * slots + 0.5)

  return "[" .. string.rep("|", filled) .. string.rep(" ", slots - filled) .. "] " .. p .. "%"
end

local function readable_name_from_id(name)
  if not name or name == "" then
    return S("Unknown")
  end

  local raw = tostring(name)
  local stripped = raw:match(":(.+)$") or raw

  stripped = stripped
    :gsub("_source$", "")
    :gsub("_flowing$", "")
    :gsub("[_%-%./]+", " ")
    :gsub("%s+", " ")
    :gsub("^%s+", "")
    :gsub("%s+$", "")

  if stripped == "" then
    return S("Unknown")
  end

  return (stripped:gsub("(%a)([%w']*)", function(a, b)
    return string.upper(a) .. string.lower(b)
  end))
end

local function liquid_name_from_node(name)
  if not name or name == "" then
    return S("Unknown")
  end

  if name:find("water", 1, true) then
    return S("Water")
  end

  if name:find("lava", 1, true) then
    return S("Lava")
  end

  return readable_name_from_id(name)
end

local ETHEREAL_SAPLING_SOILS = {
  ["ethereal:basandra_bush_sapling"] = "ethereal:fiery_dirt",
  ["ethereal:yellow_tree_sapling"] = "group:soil",
  ["ethereal:big_tree_sapling"] = "default:dirt_with_grass",
  ["ethereal:banana_tree_sapling"] = "ethereal:grove_dirt",
  ["ethereal:frost_tree_sapling"] = "ethereal:crystal_dirt",
  ["ethereal:mushroom_sapling"] = "ethereal:mushroom_dirt",
  ["ethereal:mushroom_brown_sapling"] = "ethereal:mushroom_dirt",
  ["ethereal:palm_sapling"] = "default:sand",
  ["ethereal:willow_sapling"] = "ethereal:gray_dirt",
  ["ethereal:redwood_sapling"] = "default:dirt_with_dry_grass",
  ["ethereal:giant_redwood_sapling"] = "default:dirt_with_dry_grass",
  ["ethereal:orange_tree_sapling"] = "ethereal:prairie_dirt",
  ["ethereal:birch_sapling"] = "default:dirt_with_grass",
  ["ethereal:sakura_sapling"] = "ethereal:bamboo_dirt",
  ["ethereal:olive_tree_sapling"] = "ethereal:grove_dirt",
  ["ethereal:lemon_tree_sapling"] = "ethereal:grove_dirt",
}

local function get_current_dig_result(player, nodename)
  if not player or not nodename or nodename == "" then
    return false, nil
  end

  local wielded = player:get_wielded_item()
  local can_dig, dig_time = eye_spy.tool_can_dig_node(wielded, nodename)

  if can_dig then
    return true, dig_time
  end

  local hand_can_dig, hand_dig_time = eye_spy.tool_can_dig_node(ItemStack(""), nodename)

  if hand_can_dig then
    return true, hand_dig_time
  end

  return false, nil
end

local function get_below_node(pos)
  if not pos then
    return nil, nil
  end

  local below_pos = {x = pos.x, y = pos.y - 1, z = pos.z}
  local below = minetest.get_node_or_nil(below_pos)

  if not below then
    return nil, nil
  end

  return below, minetest.registered_nodes[below.name]
end

local function is_water_node(node, def)
  local name = node and node.name or ""

  if name == "" then
    return false
  end

  if name:find("water", 1, true) then
    return true
  end

  local source_name = def and def.liquid_alternative_source or ""
  local flowing_name = def and def.liquid_alternative_flowing or ""

  return source_name:find("water", 1, true) ~= nil or flowing_name:find("water", 1, true) ~= nil
end

local function has_nearby_water(pos, radius)
  if not pos then
    return false
  end

  local scan_radius = math.max(1, tonumber(radius) or 3)

  for dx = -scan_radius, scan_radius do
    for dz = -scan_radius, scan_radius do
      for dy = -1, 1 do
        if dx ~= 0 or dy ~= 0 or dz ~= 0 then
          local neighbor = minetest.get_node_or_nil({
            x = pos.x + dx,
            y = pos.y + dy,
            z = pos.z + dz,
          })

          if neighbor then
            local neighbor_def = minetest.registered_nodes[neighbor.name]

            if is_water_node(neighbor, neighbor_def) then
              return true
            end
          end
        end
      end
    end
  end

  return false
end

local function is_soil_wet(soil_pos, soil_group)
  if (tonumber(soil_group) or 0) >= 3 then
    return true
  end

  return has_nearby_water(soil_pos, 3)
end

local function get_node_label(name)
  if not name or name == "" then
    return S("Unknown")
  end

  if name:sub(1, 6) == "group:" then
    local group_name = name:sub(7)

    if group_name == "soil" then
      return S("Soil")
    end

    return group_name
  end

  local def = minetest.registered_nodes[name]
  local desc = def and def.description

  if desc and desc ~= "" then
    return desc:match("^[^\n]*") or desc
  end

  return name
end

local function get_soil_state(pos)
  local below, below_def = get_below_node(pos)
  local soil_group = below and minetest.get_item_group(below.name, "soil") or 0
  local below_pos = pos and {x = pos.x, y = pos.y - 1, z = pos.z} or nil
  local is_wet = soil_group > 0 and is_soil_wet(below_pos, soil_group)

  return {
    node = below,
    def = below_def,
    soil_group = soil_group,
    is_soil = soil_group > 0,
    is_wet = is_wet,
  }
end

local function get_target_soil_state(target)
  local soil_group = minetest.get_item_group(target.name or "", "soil")

  if soil_group <= 0 then
    return nil
  end

  return {
    node = {
      name = target.name,
      param1 = target.node and target.node.param1 or 0,
      param2 = target.node and target.node.param2 or 0,
    },
    def = target.def,
    soil_group = soil_group,
    is_soil = true,
    is_wet = is_soil_wet(target.pos, soil_group),
  }
end

local function infer_max_stage(base_name, current_stage)
  local max_stage = current_stage or 0

  for stage = max_stage + 1, 16 do
    if minetest.registered_nodes[base_name .. "_" .. stage] then
      max_stage = stage
    else
      break
    end
  end

  if max_stage > 0 then
    return max_stage
  end
end

local function get_registered_crop_info(base_name)
  if farming and farming.registered_plants and farming.registered_plants[base_name] then
    local plant = farming.registered_plants[base_name]

    return {
      system = "farming",
      steps = plant.steps,
      minlight = plant.minlight or (farming and farming.min_light) or 12,
      maxlight = plant.maxlight or (farming and farming.max_light) or 15,
    }
  end

  if x_farming and x_farming.registered_plants then
    local short_name = base_name:match(":(.+)$")
    local plant = short_name and x_farming.registered_plants[short_name]

    if plant then
      return {
        system = "x_farming",
        steps = plant.steps,
        minlight = plant.minlight or 13,
        maxlight = plant.maxlight or 14,
        fertility = plant.fertility,
      }
    end
  end
end

local function soil_matches_fertility(soil_state, fertility)
  if not fertility or #fertility == 0 then
    return true
  end

  local below_def = soil_state.def
  local groups = below_def and below_def.groups or nil

  if not groups then
    return false
  end

  for _, group_name in ipairs(fertility) do
    if groups[group_name] and groups[group_name] > 0 then
      return true
    end
  end

  return false
end

local function get_growth_light(pos)
  if not pos then
    return nil
  end

  return minetest.get_node_light(pos)
end

local function get_growth_info(target)
  local def = target.def
  local groups = def and def.groups or {}

  if groups.sapling then
    local meta = minetest.get_meta(target.pos)

    return {
      kind = "sapling",
      age = meta and meta:get_int("age") or 0,
      groups = groups,
    }
  end

  local stage_suffix = target.name:match("_(%d+)$")
  local base_name = stage_suffix and target.name:gsub("_(%d+)$", "", 1) or nil
  local plant = base_name and get_registered_crop_info(base_name) or nil

  if not stage_suffix or not (groups.plant or groups.crop or groups.growing or plant) then
    return nil
  end

  local stage = tonumber(stage_suffix) or 0
  plant = plant or {}
  local max_stage = plant.steps or infer_max_stage(base_name, stage)

  if not max_stage or max_stage <= 0 then
    return nil
  end

  return {
    kind = "crop",
    system = plant.system or "generic",
    stage = stage,
    max_stage = max_stage,
    minlight = plant.minlight or def.minlight,
    maxlight = plant.maxlight or def.maxlight,
    fertility = plant.fertility,
  }
end

local function get_sapling_soil_requirement(target)
  local requirement = ETHEREAL_SAPLING_SOILS[target.name]

  if requirement then
    return requirement
  end

  if target.name == "x_farming:kiwi_sapling" then
    return "group:soil"
  end

  return "group:soil"
end

local function soil_matches_requirement(soil_state, requirement)
  if not requirement or requirement == "" then
    return true
  end

  if requirement:sub(1, 6) == "group:" then
    return minetest.get_item_group(soil_state.node and soil_state.node.name or "", requirement:sub(7)) > 0
  end

  return soil_state.node and soil_state.node.name == requirement or false
end

local function get_crop_conditions(target, growth_info)
  local soil_state = get_soil_state(target.pos)
  local light = target.light_level

  if light == nil then
    light = get_growth_light(target.pos)
  end
  local minlight = growth_info.minlight
  local maxlight = growth_info.maxlight
  local fertility_ok = soil_matches_fertility(soil_state, growth_info.fertility)
  local soil_ok = soil_state.is_wet and fertility_ok
  local light_ok = true

  if minlight and light and light < minlight then
    light_ok = false
  end

  if maxlight and light and light > maxlight then
    light_ok = false
  end

  if light == nil and (minlight or maxlight) then
    light_ok = false
  end

  return {
    soil = soil_state,
    light = light,
    soil_ok = soil_ok,
    light_ok = light_ok,
    fertility_ok = fertility_ok,
  }
end

local function get_soil_line(target, growth_info)
  local soil_state = get_soil_state(target.pos)
  local target_groups = target.def and target.def.groups or {}

  if growth_info and growth_info.kind == "crop" then
    local suitable = soil_state.is_soil and soil_matches_fertility(soil_state, growth_info.fertility)
    local moisture_label

    if soil_state.is_wet then
      moisture_label = S("Wet")
    elseif soil_state.is_soil then
      moisture_label = S("Dry")
    else
      moisture_label = S("No Soil")
    end

    local color

    if suitable and soil_state.is_wet then
      color = 0x6BD66B
    elseif suitable then
      color = 0xFFAA55
    else
      color = 0xE56363
    end

    return {
      text = S("Soil") .. ": " .. moisture_label .. " | " .. (suitable and S("Suitable") or S("Unsuitable")),
      color = color,
    }
  end

  if growth_info and growth_info.kind == "sapling" then
    local requirement = get_sapling_soil_requirement(target)
    local suitable = soil_matches_requirement(soil_state, requirement)

    return {
      text = S("Soil") .. ": " .. get_node_label(requirement) .. " | " .. (suitable and S("Suitable") or S("Unsuitable")),
      color = suitable and 0x6BD66B or 0xE56363,
    }
  end

  if target_groups.flora or target_groups.attached_node then
    return nil
  end

  local target_soil_state = get_target_soil_state(target)

  if target_soil_state then
    return {
      text = S("Soil") .. ": " .. (target_soil_state.is_wet and S("Wet") or S("Dry")),
      color = target_soil_state.is_wet and 0x5555FF or 0xFFAA55,
    }
  end
end

function eye_spy.enrichers.register(id, spec)
  eye_spy.enrichers.registry[#eye_spy.enrichers.registry + 1] = {
    id = id,
    enabled = spec.enabled,
    apply = spec.apply,
  }
end

local function ensure_shared_context(context, target)
  local shared = context.shared

  if shared and shared._target_key == (target.key or target.name) then
    return shared
  end

  shared = {
    _target_key = target.key or target.name,
    dig_cached = false,
    growth_cached = false,
    conditions_cached = false,
    soil_line_cached = false,
  }

  function shared:get_dig_result()
    if not self.dig_cached then
      self.dig_cached = true
      self.can_dig, self.dig_time = get_current_dig_result(context.player, target.name)
    end

    return self.can_dig, self.dig_time
  end

  function shared:get_growth_info()
    if not self.growth_cached then
      self.growth_cached = true
      self.growth_info = get_growth_info(target)
    end

    return self.growth_info
  end

  function shared:get_crop_conditions()
    if not self.conditions_cached then
      self.conditions_cached = true
      local growth_info = self:get_growth_info()

      if growth_info and growth_info.kind == "crop" then
        self.crop_conditions = get_crop_conditions(target, growth_info)
      else
        self.crop_conditions = nil
      end
    end

    return self.crop_conditions
  end

  function shared:get_soil_line()
    if not self.soil_line_cached then
      self.soil_line_cached = true
      self.soil_line = get_soil_line(target, self:get_growth_info())
    end

    return self.soil_line
  end

  context.shared = shared
  return shared
end

local function get_cached_node_tool(context, target)
  local player = context and context.player

  if not player then
    return nil
  end

  local shared = ensure_shared_context(context, target)
  local player_name = player:get_player_name()
  local wielded = player:get_wielded_item()
  local wield_name = wielded:get_name() or ""
  local target_key = target.key or target.name or ""
  local cached = node_tool_state_by_player[player_name]

  if cached and cached.target_key == target_key and cached.wield_name == wield_name then
    return cached.tool
  end

  local can_dig = shared:get_dig_result()
  local tool_type_hint, matches_tool_type = nil, false

  if eye_spy.get_node_tool_hint_and_match then
    tool_type_hint, matches_tool_type = eye_spy.get_node_tool_hint_and_match(target.name, wielded)
  end

  local label = tool_type_hint or S("Unknown")
  local color

  if can_dig then
    color = 0x6BD66B
  elseif matches_tool_type then
    color = 0xFFAA55
  else
    color = 0xE56363
  end

  local tool = {
    description = label,
    color = color,
  }

  node_tool_state_by_player[player_name] = {
    target_key = target_key,
    wield_name = wield_name,
    tool = tool,
  }

  return tool
end

function eye_spy.enrichers.run(context, target, view_model)
  ensure_shared_context(context, target)

  for _, enricher in ipairs(eye_spy.enrichers.registry) do
    if not enricher.enabled or enricher.enabled(context, target, view_model) then
      local perf_on = eye_spy.perf and eye_spy.perf.enabled and minetest.get_us_time
      local start_us = perf_on and minetest.get_us_time() or 0

      enricher.apply(context, target, view_model)

      if perf_on then
        eye_spy.perf_record("enricher_" .. (enricher.id or "unknown"), minetest.get_us_time() - start_us)
      end
    end
  end
end

eye_spy.enrichers.register("node_tool", {
  enabled = function(_, target)
    return target.kind == "node"
  end,
  apply = function(context, target, view_model)
    local tool = get_cached_node_tool(context, target) or {
      description = S("Unknown"),
      color = 0xEBB344,
    }

    view_model.subtitle = tool.description ~= "" and tool.description or S("Unknown")
    view_model.subtitle_color = tool.color or 0xEBB344
  end,
})

eye_spy.enrichers.register("entity_health", {
  enabled = function(_, target)
    return target.kind == "entity"
  end,
  apply = function(context, target, view_model)
    if context.meta:get_string("es_hud_health_in") == "Hearts" then
      view_model.subtitle = S("Health") .. ": " .. (eye_spy.round1((target.hp or 0) / 2) or S("Unknown")) .. "♥/" .. (eye_spy.round1((target.max_hp or 0) / 2) or S("Unknown")) .. "♥"
    else
      view_model.subtitle = S("Health") .. ": " .. (eye_spy.round1(target.hp) or S("Unknown")) .. "/" .. (eye_spy.round1(target.max_hp) or S("Unknown"))
    end

    view_model.subtitle_color = 0xFF4040
  end,
})

eye_spy.enrichers.register("item_details", {
  enabled = function(_, target)
    return target.kind == "item"
  end,
  apply = function(_, target, view_model)
    local count = tonumber(target.stack_count) or 1

    if count > 1 then
      add_line(view_model, "item_count", S("Count") .. ": " .. tostring(count), 0xE8D37A)
    end

    local wear = tonumber(target.stack_wear) or 0

    if wear > 0 then
      local remain_pct = math.floor(math.max(0, 1 - (wear / 65535)) * 100 + 0.5)
      local wear_color = 0xE56363

      if remain_pct >= 60 then
        wear_color = 0x6BD66B
      elseif remain_pct >= 30 then
        wear_color = 0xF0B35E
      end

      add_line(view_model, "item_wear", S("Durability") .. ": " .. tostring(remain_pct) .. "%", wear_color)
    end

    if target.item_def_description and target.item_def_description ~= "" then
      local lines = {}
      for line in target.item_def_description:gmatch("[^\n]+") do
        table.insert(lines, line)
      end
      
      for i = 2, #lines do
        add_line(view_model, "item_def_line_" .. i, lines[i], 0xCCCCCC)
      end
    end

    if target.stack_has_meta then
      local detail = target.stack_meta_summary

      if detail and detail ~= "" then
        add_line(view_model, "item_meta", S("Metadata") .. ": " .. detail, 0x98C9FF)
      else
        add_line(view_model, "item_meta", S("Metadata"), 0x98C9FF)
      end
    end
  end,
})

eye_spy.enrichers.register("growth", {
  enabled = function(context, target)
    return target.kind == "node" and meta_bool(context.meta, "es_show_growth", eye_spy.config.show_growth)
  end,
  apply = function(context, target, view_model)
    local shared = ensure_shared_context(context, target)
    local growth_info = shared:get_growth_info()

    if not growth_info then
      return
    end

    if growth_info.kind == "sapling" then
      local age = growth_info.age or 0

      if age > 0 then
        local percent = math.min(math.floor((age / 20) * 100), 100)
        add_line(view_model, "growth", S("Maturity") .. ": " .. growth_bar(percent), 0xAAAAFF)
      else
        add_line(view_model, "growth", S("Status") .. ": " .. S("Rooting"), 0x888888)
      end

      return
    end

    if growth_info.kind == "crop" then
      local percent = math.floor(math.min(growth_info.stage / math.max(growth_info.max_stage, 1), 1) * 100)
      local conditions = shared:get_crop_conditions() or get_crop_conditions(target, growth_info)
      local stalled = growth_info.stage < growth_info.max_stage and (not conditions.soil_ok or not conditions.light_ok)
      local status = ""

      if stalled then
        local reasons = {}

        if not conditions.soil_ok then
          reasons[#reasons + 1] = conditions.soil.is_soil and S("Dry Soil") or S("Bad Soil")
        end

        if not conditions.light_ok then
          reasons[#reasons + 1] = S("Bad Light")
        end

        status = " (" .. S("Stalled")

        if #reasons > 0 then
          status = status .. ": " .. table.concat(reasons, ", ")
        end

        status = status .. ")"
      end

      add_line(
        view_model,
        "growth",
        S("Growth") .. ": " .. growth_bar(percent) .. status,
        stalled and 0xFFAA55 or 0x55FF55
      )
    end
  end,
})

eye_spy.enrichers.register("dig_time", {
  enabled = function(context, target)
    return target.kind == "node" and meta_bool(context.meta, "es_show_dig_time", true)
  end,
  apply = function(context, target, view_model)
    local shared = ensure_shared_context(context, target)
    local diggable, time = shared:get_dig_result()

    if not diggable then
      add_line(view_model, "dig_time", S("Dig Time") .. ": ∞", 0xA9C5FF)
      return
    end

    local t = tonumber(time) or 0
    add_line(view_model, "dig_time", S("Dig Time") .. ": " .. string.format("%.2fs", t), 0xA9C5FF)
  end,
})

eye_spy.enrichers.register("soil", {
  enabled = function(context, target)
    return target.kind == "node" and meta_bool(context.meta, "es_show_growth", eye_spy.config.show_growth)
  end,
  apply = function(context, target, view_model)
    local shared = ensure_shared_context(context, target)
    local soil_line = shared:get_soil_line()

    if not soil_line then
      return
    end

    add_line(view_model, "soil", soil_line.text, soil_line.color)
  end,
})

eye_spy.enrichers.register("light_level", {
  enabled = function(context, target)
    return target.kind == "node" and meta_bool(context.meta, "es_show_light_level", true)
  end,
  apply = function(_, target, view_model)
    if target.light_level == nil then
      add_line(view_model, "light", S("Light") .. ": " .. S("Unknown"), 0xAAAAAA)
      return
    end

    add_line(view_model, "light", S("Light") .. ": " .. tostring(target.light_level), 0xF2DD88)
  end,
})

eye_spy.enrichers.register("spawn_risk", {
  enabled = function(context, target)
    return target.kind == "node" and meta_bool(context.meta, "es_show_spawn_hint", true)
  end,
  apply = function(context, target, view_model)
    if target.light_level == nil then
      add_line(view_model, "spawn", S("Spawn Risk") .. " (" .. S("Est") .. "): " .. S("Unknown"), 0xAAAAAA)
      return
    end

    local threshold = context.meta:get_int("es_spawn_safe_light_threshold")

    if threshold < 0 or threshold > 15 then
      threshold = 8
    end

    if target.light_level <= threshold then
      add_line(view_model, "spawn", S("Spawn Risk") .. " (" .. S("Est") .. "): " .. S("Dark Enough"), 0xF08A55)
    else
      add_line(view_model, "spawn", S("Spawn Risk") .. " (" .. S("Est") .. "): " .. S("Likely Safe"), 0x6BD66B)
    end
  end,
})

eye_spy.enrichers.register("liquid_info", {
  enabled = function(context, target)
    if not eye_spy.config.show_liquids then
      return false
    end

    local show_liquid = meta_bool(context.meta, "es_show_liquid_info", true)

    if not show_liquid then
      return false
    end

    return target.is_liquid or target.liquid_obstacle ~= nil
  end,
  apply = function(_, target, view_model)
    local liquid_target = target.is_liquid and target or target.liquid_obstacle

    if not liquid_target then
      return
    end

    local name = liquid_name_from_node(liquid_target.full_name or liquid_target.name)
    local detail

    if liquid_target.liquid_type == "source" then
      detail = S("Source")
    elseif liquid_target.liquid_level then
      detail = S("Level") .. ": " .. tostring(liquid_target.liquid_level)
    else
      detail = S("Flowing")
    end

    add_line(view_model, "liquid", S("Liquid") .. ": " .. name .. " (" .. detail .. ")", 0x6FB8FF)
  end,
})