---@class MemoryScanner
---@field _StateManager table
---@field _Context table
---@field _ImageCache table
---@field _ErrorHandler table
local MemoryScanner = {}

---Initialize MemoryScanner with dependencies
---@param deps {StateManager: table, Context: table, ImageCache: table, ErrorHandler: table}
function MemoryScanner.init(deps)
  MemoryScanner._StateManager = deps.StateManager
  MemoryScanner._Context = deps.Context
  MemoryScanner._ImageCache = deps.ImageCache
  MemoryScanner._ErrorHandler = deps.ErrorHandler
end

---Count items in a table
---@param tbl table
---@return number
local function countTable(tbl)
  local count = 0
  for _ in pairs(tbl) do
    count = count + 1
  end
  return count
end

---Calculate memory size estimate for a table (recursive)
---@param tbl table
---@param visited table? Tracking table to prevent circular references
---@param depth number? Current recursion depth
---@return number bytes Estimated memory usage in bytes
local function estimateTableSize(tbl, visited, depth)
  if type(tbl) ~= "table" then
    return 0
  end

  visited = visited or {}
  depth = depth or 0

  -- Limit recursion depth to prevent stack overflow
  if depth > 10 then
    return 0
  end

  -- Check for circular references
  if visited[tbl] then
    return 0
  end
  visited[tbl] = true

  local size = 40 -- Base table overhead (approximate)

  for k, v in pairs(tbl) do
    -- Key size
    if type(k) == "string" then
      size = size + #k + 24 -- String overhead
    elseif type(k) == "number" then
      size = size + 8
    else
      size = size + 8 -- Reference
    end

    -- Value size
    if type(v) == "string" then
      size = size + #v + 24
    elseif type(v) == "number" then
      size = size + 8
    elseif type(v) == "boolean" then
      size = size + 4
    elseif type(v) == "table" then
      size = size + estimateTableSize(v, visited, depth + 1)
    elseif type(v) == "function" then
      size = size + 16 -- Function reference
    else
      size = size + 8 -- Other references
    end
  end

  return size
end

---Scan StateManager for memory issues
---@return table report Detailed report of StateManager memory usage
function MemoryScanner.scanStateManager()
  local report = {
    stateCount = 0,
    stateStoreSize = 0,
    metadataSize = 0,
    callSiteCounterSize = 0,
    orphanedStates = {},
    staleStates = {},
    largeStates = {},
    issues = {},
  }

  if not MemoryScanner._StateManager then
    table.insert(report.issues, {
      severity = "error",
      message = "StateManager not initialized",
    })
    return report
  end

  local internal = MemoryScanner._StateManager._getInternalState()
  local stateStore = internal.stateStore
  local stateMetadata = internal.stateMetadata
  local callSiteCounters = internal.callSiteCounters
  local currentFrame = MemoryScanner._StateManager.getFrameNumber()

  -- Count states
  report.stateCount = countTable(stateStore)

  -- Estimate sizes
  report.stateStoreSize = estimateTableSize(stateStore)
  report.metadataSize = estimateTableSize(stateMetadata)
  report.callSiteCounterSize = estimateTableSize(callSiteCounters)

  -- Check for orphaned states (metadata without state)
  for id, _ in pairs(stateMetadata) do
    if not stateStore[id] then
      table.insert(report.orphanedStates, id)
    end
  end

  -- Check for stale states (not accessed in many frames)
  local staleThreshold = 120 -- 2 seconds at 60fps
  for id, meta in pairs(stateMetadata) do
    local framesSinceAccess = currentFrame - meta.lastFrame
    if framesSinceAccess > staleThreshold then
      table.insert(report.staleStates, {
        id = id,
        framesSinceAccess = framesSinceAccess,
        createdFrame = meta.createdFrame,
        accessCount = meta.accessCount,
      })
    end
  end

  -- Check for large states (may indicate memory bloat)
  for id, state in pairs(stateStore) do
    local stateSize = estimateTableSize(state)
    if stateSize > 1024 then -- More than 1KB
      table.insert(report.largeStates, {
        id = id,
        size = stateSize,
        keyCount = countTable(state),
      })
    end
  end

  -- Check callSiteCounters (should be near 0 after frame cleanup)
  local callSiteCount = countTable(callSiteCounters)
  if callSiteCount > 100 then
    table.insert(report.issues, {
      severity = "warning",
      message = string.format("callSiteCounters has %d entries (expected near 0)", callSiteCount),
      suggestion = "incrementFrame() may not be called properly, or counters aren't being reset",
    })
  end

  -- Check for excessive state count
  if report.stateCount > 500 then
    table.insert(report.issues, {
      severity = "warning",
      message = string.format("High state count: %d states", report.stateCount),
      suggestion = "Consider reducing element count or implementing more aggressive cleanup",
    })
  end

  -- Check for orphaned states
  if #report.orphanedStates > 0 then
    table.insert(report.issues, {
      severity = "error",
      message = string.format("Found %d orphaned states (metadata without state)", #report.orphanedStates),
      suggestion = "This indicates a bug in state management - metadata should be cleaned up with state",
    })
  end

  -- Check for stale states
  if #report.staleStates > 10 then
    table.insert(report.issues, {
      severity = "warning",
      message = string.format("Found %d stale states (not accessed in 2+ seconds)", #report.staleStates),
      suggestion = "Cleanup may not be aggressive enough - consider reducing stateRetentionFrames",
    })
  end

  return report
end

---Scan Context for memory issues
---@return table report Detailed report of Context memory usage
function MemoryScanner.scanContext()
  local report = {
    topElementCount = 0,
    zIndexElementCount = 0,
    frameElementCount = 0,
    issues = {},
  }

  if not MemoryScanner._Context then
    table.insert(report.issues, {
      severity = "error",
      message = "Context not initialized",
    })
    return report
  end

  -- Count elements
  report.topElementCount = #MemoryScanner._Context.topElements
  report.zIndexElementCount = #MemoryScanner._Context._zIndexOrderedElements
  report.frameElementCount = #MemoryScanner._Context._currentFrameElements

  -- Check for stale z-index elements (should be cleared each frame)
  if MemoryScanner._Context._immediateMode then
    -- In immediate mode, _zIndexOrderedElements should be cleared at frame start
    -- If it has elements outside of frame rendering, that's a leak
    if not MemoryScanner._Context._frameStarted and report.zIndexElementCount > 0 then
      table.insert(report.issues, {
        severity = "warning",
        message = string.format("Z-index array has %d elements outside of frame", report.zIndexElementCount),
        suggestion = "clearFrameElements() may not be called properly in beginFrame()",
      })
    end
  end

  -- Check for excessive element count
  if report.topElementCount > 100 then
    table.insert(report.issues, {
      severity = "info",
      message = string.format("High top-level element count: %d", report.topElementCount),
      suggestion = "Consider consolidating elements or using fewer top-level containers",
    })
  end

  return report
end

---Scan ImageCache for memory issues
---@return table report Detailed report of ImageCache memory usage
function MemoryScanner.scanImageCache()
  local report = {
    imageCount = 0,
    estimatedMemory = 0,
    issues = {},
  }

  if not MemoryScanner._ImageCache then
    table.insert(report.issues, {
      severity = "error",
      message = "ImageCache not initialized",
    })
    return report
  end

  local stats = MemoryScanner._ImageCache.getStats()
  report.imageCount = stats.count
  report.estimatedMemory = stats.memoryEstimate

  -- Check for excessive memory usage (>100MB)
  if report.estimatedMemory > 100 * 1024 * 1024 then
    table.insert(report.issues, {
      severity = "warning",
      message = string.format("ImageCache using ~%.2f MB", report.estimatedMemory / 1024 / 1024),
      suggestion = "Consider implementing cache eviction or clearing unused images",
    })
  end

  -- Check for excessive image count
  if report.imageCount > 50 then
    table.insert(report.issues, {
      severity = "info",
      message = string.format("ImageCache has %d images", report.imageCount),
      suggestion = "Review if all cached images are necessary",
    })
  end

  return report
end

---Check if a circular reference is intentional (parent-child, module, or metatable)
---@param path string The current path where circular ref was detected
---@param originalPath string The original path where the table was first seen
---@return boolean True if this is an intentional circular reference
local function isIntentionalCircularReference(path, originalPath)
  -- Pattern 1: child.parent points back to parent
  -- Example: "topElements.1.children.1.parent" -> "topElements.1"
  if path:match("%.parent$") then
    local parentPath = path:match("^(.+)%.children%.[^.]+%.parent$")
    if parentPath == originalPath then
      return true
    end
  end

  -- Pattern 2: parent.children[n] points to child, child points back somewhere in parent tree
  -- Example: "topElements.1" -> "topElements.1.children.1.parent"
  if originalPath:match("%.parent$") then
    local childParentPath = originalPath:match("^(.+)%.children%.[^.]+%.parent$")
    if childParentPath == path then
      return true
    end
  end

  -- Pattern 3: Check for nested parent-child cycles
  -- child.children[n].parent -> child
  local segments = {}
  for segment in path:gmatch("[^.]+") do
    table.insert(segments, segment)
  end

  -- Look for .children.N.parent pattern
  for i = 1, #segments - 2 do
    if segments[i] == "children" and segments[i + 2] == "parent" then
      -- Reconstruct path without the .children.N.parent suffix
      local reconstructedPath = table.concat(segments, ".", 1, i - 1)
      if reconstructedPath == originalPath then
        return true
      end
    end
  end

  -- Pattern 4: Metatable __index self-references (modules)
  -- Example: "element._renderer._Theme.__index" -> "element._renderer._Theme"
  if path:match("%.__index$") then
    local basePath = path:match("^(.+)%.__index$")
    if basePath == originalPath then
      return true
    end
  end

  -- Pattern 5: Shared module references (elements sharing same module instances)
  -- Example: Multiple elements referencing _utils, _Theme, _Blur, etc.
  -- These start with _ and are typically modules
  local pathModuleName = path:match("%.(_[%w]+)%.")
  local originalModuleName = originalPath:match("%.(_[%w]+)%.")
  
  if pathModuleName and originalModuleName then
    -- If both paths reference the same internal module (starting with _), it's intentional
    if pathModuleName == originalModuleName then
      return true
    end
  end

  -- Pattern 6: Shared Color/Transform objects between elements
  -- These are value objects that can be safely shared
  if path:match("Color") and originalPath:match("Color") then
    return true
  end
  if path:match("Transform") and originalPath:match("Transform") then
    return true
  end

  -- Pattern 7: LayoutEngine holding reference to its element
  -- Example: "element._layoutEngine.element" -> "element"
  if path:match("%._layoutEngine%.element$") then
    local elementPath = path:match("^(.+)%._layoutEngine%.element$")
    if elementPath == originalPath then
      return true
    end
  end

  -- Pattern 8: Renderer holding references to element properties
  -- Example: "element._renderer.cornerRadius" -> "element.cornerRadius"
  if path:match("%._renderer%.") then
    local rendererBasePath = path:match("^(.+)%._renderer%.")
    local originalBasePath = originalPath:match("^(.+)%.")
    if rendererBasePath == originalBasePath then
      return true
    end
  end

  -- Pattern 9: Context reference from layout engine (shared singleton)
  -- Example: "element._layoutEngine._Context.topElements" -> "topElements"
  if path:match("%._layoutEngine%._Context%.") and originalPath == "topElements" then
    return true
  end

  return false
end

---Detect circular references in a table
---@param tbl table Table to check
---@param path string? Current path (for reporting)
---@param visited table? Tracking table
---@return table[] circularRefs Array of circular reference paths
---@return table[] intentionalRefs Array of intentional parent-child refs
local function detectCircularReferences(tbl, path, visited)
  if type(tbl) ~= "table" then
    return {}, {}
  end

  path = path or "root"
  visited = visited or {}
  local circularRefs = {}
  local intentionalRefs = {}

  -- Check if we've seen this table before
  if visited[tbl] then
    local ref = {
      path = path,
      originalPath = visited[tbl],
    }

    -- Determine if this is an intentional circular reference
    if isIntentionalCircularReference(path, visited[tbl]) then
      table.insert(intentionalRefs, ref)
    else
      table.insert(circularRefs, ref)
    end

    return circularRefs, intentionalRefs
  end

  -- Mark as visited
  visited[tbl] = path

  -- Recursively check children
  for k, v in pairs(tbl) do
    if type(v) == "table" then
      local childPath = path .. "." .. tostring(k)
      local childRefs, childIntentionalRefs = detectCircularReferences(v, childPath, visited)
      for _, ref in ipairs(childRefs) do
        table.insert(circularRefs, ref)
      end
      for _, ref in ipairs(childIntentionalRefs) do
        table.insert(intentionalRefs, ref)
      end
    end
  end

  return circularRefs, intentionalRefs
end

---Scan for circular references in immediate mode
---@return table report Detailed report of circular references
function MemoryScanner.scanCircularReferences()
  local report = {
    stateStoreCircularRefs = {},
    stateStoreIntentionalRefs = {},
    contextCircularRefs = {},
    contextIntentionalRefs = {},
    issues = {},
  }

  if MemoryScanner._StateManager then
    local internal = MemoryScanner._StateManager._getInternalState()
    report.stateStoreCircularRefs, report.stateStoreIntentionalRefs = detectCircularReferences(internal.stateStore, "stateStore")
  end

  if MemoryScanner._Context then
    report.contextCircularRefs, report.contextIntentionalRefs = detectCircularReferences(MemoryScanner._Context.topElements, "topElements")
  end

  -- Report issues only for cross-module circular references
  if #report.stateStoreCircularRefs > 0 then
    table.insert(report.issues, {
      severity = "info",
      message = string.format("Found %d cross-module circular references in StateManager", #report.stateStoreCircularRefs),
      suggestion = "These are typically architectural dependencies between modules, not memory leaks",
    })
  end

  if #report.contextCircularRefs > 0 then
    table.insert(report.issues, {
      severity = "info",
      message = string.format("Found %d cross-module circular references in Context", #report.contextCircularRefs),
      suggestion = "These are typically architectural dependencies (e.g., layout engine ↔ renderer), not memory leaks",
    })
  end

  return report
end

---Run comprehensive memory scan
---@return table report Complete memory analysis report
function MemoryScanner.scan()
  local startMemory = collectgarbage("count")

  local report = {
    timestamp = os.time(),
    startMemory = startMemory / 1024, -- MB
    stateManager = MemoryScanner.scanStateManager(),
    context = MemoryScanner.scanContext(),
    imageCache = MemoryScanner.scanImageCache(),
    circularRefs = MemoryScanner.scanCircularReferences(),
    summary = {
      totalIssues = 0,
      criticalIssues = 0,
      warnings = 0,
      info = 0,
    },
  }

  -- Count issues by severity
  local function countIssues(subReport)
    for _, issue in ipairs(subReport.issues or {}) do
      report.summary.totalIssues = report.summary.totalIssues + 1
      if issue.severity == "error" then
        report.summary.criticalIssues = report.summary.criticalIssues + 1
      elseif issue.severity == "warning" then
        report.summary.warnings = report.summary.warnings + 1
      elseif issue.severity == "info" then
        report.summary.info = report.summary.info + 1
      end
    end
  end

  countIssues(report.stateManager)
  countIssues(report.context)
  countIssues(report.imageCache)
  countIssues(report.circularRefs)

  -- Force GC and measure freed memory
  local beforeGC = collectgarbage("count")
  collectgarbage("collect")
  collectgarbage("collect")
  local afterGC = collectgarbage("count")

  report.gcAnalysis = {
    beforeGC = beforeGC / 1024, -- MB
    afterGC = afterGC / 1024, -- MB
    freed = (beforeGC - afterGC) / 1024, -- MB
    freedPercent = ((beforeGC - afterGC) / beforeGC) * 100,
  }

  -- Analyze GC effectiveness
  if report.gcAnalysis.freedPercent < 5 then
    table.insert(report.stateManager.issues, {
      severity = "info",
      message = string.format("GC freed only %.1f%% of memory", report.gcAnalysis.freedPercent),
      suggestion = "Most memory is still referenced - this is normal if UI is active",
    })
  elseif report.gcAnalysis.freedPercent > 30 then
    table.insert(report.stateManager.issues, {
      severity = "warning",
      message = string.format("GC freed %.1f%% of memory", report.gcAnalysis.freedPercent),
      suggestion = "Significant memory was unreferenced - may indicate cleanup issues",
    })
  end

  return report
end

---Format report as human-readable string
---@param report table Memory scan report
---@return string formatted Formatted report
function MemoryScanner.formatReport(report)
  local lines = {}

  table.insert(lines, "=== FlexLöve Memory Scanner Report ===")
  table.insert(lines, string.format("Timestamp: %s", os.date("%Y-%m-%d %H:%M:%S", report.timestamp)))
  table.insert(lines, string.format("Memory: %.2f MB", report.startMemory))
  table.insert(lines, "")

  -- Summary
  table.insert(lines, "--- Summary ---")
  table.insert(lines, string.format("Total Issues: %d", report.summary.totalIssues))
  table.insert(lines, string.format("  Critical: %d", report.summary.criticalIssues))
  table.insert(lines, string.format("  Warnings: %d", report.summary.warnings))
  table.insert(lines, string.format("  Info: %d", report.summary.info))
  table.insert(lines, "")

  -- StateManager
  table.insert(lines, "--- StateManager ---")
  table.insert(lines, string.format("State Count: %d", report.stateManager.stateCount))
  table.insert(lines, string.format("State Store Size: %.2f KB", report.stateManager.stateStoreSize / 1024))
  table.insert(lines, string.format("Metadata Size: %.2f KB", report.stateManager.metadataSize / 1024))
  table.insert(lines, string.format("CallSite Counters: %.2f KB", report.stateManager.callSiteCounterSize / 1024))
  table.insert(lines, string.format("Orphaned States: %d", #report.stateManager.orphanedStates))
  table.insert(lines, string.format("Stale States: %d", #report.stateManager.staleStates))
  table.insert(lines, string.format("Large States: %d", #report.stateManager.largeStates))

  if #report.stateManager.issues > 0 then
    table.insert(lines, "Issues:")
    for _, issue in ipairs(report.stateManager.issues) do
      table.insert(lines, string.format("  [%s] %s", string.upper(issue.severity), issue.message))
      if issue.suggestion then
        table.insert(lines, string.format("    → %s", issue.suggestion))
      end
    end
  end
  table.insert(lines, "")

  -- Context
  table.insert(lines, "--- Context ---")
  table.insert(lines, string.format("Top Elements: %d", report.context.topElementCount))
  table.insert(lines, string.format("Z-Index Elements: %d", report.context.zIndexElementCount))
  table.insert(lines, string.format("Frame Elements: %d", report.context.frameElementCount))

  if #report.context.issues > 0 then
    table.insert(lines, "Issues:")
    for _, issue in ipairs(report.context.issues) do
      table.insert(lines, string.format("  [%s] %s", string.upper(issue.severity), issue.message))
      if issue.suggestion then
        table.insert(lines, string.format("    → %s", issue.suggestion))
      end
    end
  end
  table.insert(lines, "")

  -- ImageCache
  table.insert(lines, "--- ImageCache ---")
  table.insert(lines, string.format("Image Count: %d", report.imageCache.imageCount))
  table.insert(lines, string.format("Estimated Memory: %.2f MB", report.imageCache.estimatedMemory / 1024 / 1024))

  if #report.imageCache.issues > 0 then
    table.insert(lines, "Issues:")
    for _, issue in ipairs(report.imageCache.issues) do
      table.insert(lines, string.format("  [%s] %s", string.upper(issue.severity), issue.message))
      if issue.suggestion then
        table.insert(lines, string.format("    → %s", issue.suggestion))
      end
    end
  end
  table.insert(lines, "")

  -- Circular References
  table.insert(lines, "--- Circular References ---")
  table.insert(lines, string.format("StateStore (Cross-module refs): %d", #report.circularRefs.stateStoreCircularRefs))
  table.insert(lines, string.format("StateStore (Intentional - parent-child, modules, metatables): %d", #report.circularRefs.stateStoreIntentionalRefs))
  table.insert(lines, string.format("Context (Cross-module refs): %d", #report.circularRefs.contextCircularRefs))
  table.insert(lines, string.format("Context (Intentional - parent-child, modules, metatables): %d", #report.circularRefs.contextIntentionalRefs))

  if #report.circularRefs.issues > 0 then
    table.insert(lines, "Issues:")
    for _, issue in ipairs(report.circularRefs.issues) do
      table.insert(lines, string.format("  [%s] %s", string.upper(issue.severity), issue.message))
      if issue.suggestion then
        table.insert(lines, string.format("    → %s", issue.suggestion))
      end
    end
  else
    table.insert(lines, "  ✓ No unexpected circular references detected")
  end
  table.insert(lines, "  Note: Cross-module refs are typically architectural dependencies, not memory leaks")
  table.insert(lines, "")

  -- GC Analysis
  table.insert(lines, "--- Garbage Collection Analysis ---")
  table.insert(lines, string.format("Before GC: %.2f MB", report.gcAnalysis.beforeGC))
  table.insert(lines, string.format("After GC: %.2f MB", report.gcAnalysis.afterGC))
  table.insert(lines, string.format("Freed: %.2f MB (%.1f%%)", report.gcAnalysis.freed, report.gcAnalysis.freedPercent))
  table.insert(lines, "")

  table.insert(lines, "=== End Report ===")

  return table.concat(lines, "\n")
end

---Save report to file
---@param report table Memory scan report
---@param filename string? Output filename (default: memory_report.txt)
function MemoryScanner.saveReport(report, filename)
  filename = filename or "memory_report.txt"
  local formatted = MemoryScanner.formatReport(report)

  local file = io.open(filename, "w")
  if file then
    file:write(formatted)
    file:close()
    print(string.format("[MemoryScanner] Report saved to %s", filename))
  else
    print(string.format("[MemoryScanner] Failed to save report to %s", filename))
  end
end

return MemoryScanner
