---@class PerformanceProfiler
---@field _frameCount number
---@field _startTime number
---@field _frameTimes table
---@field _fpsHistory table
---@field _memoryHistory table
---@field _customMetrics table
---@field _markers table
---@field _currentFrameStart number?
---@field _maxHistorySize number
---@field _lastGcCount number
---@field _snapshots table
---@field _currentSnapshot table?
local PerformanceProfiler = {}
PerformanceProfiler.__index = PerformanceProfiler

---@param config {maxHistorySize: number?}?
---@return PerformanceProfiler
function PerformanceProfiler.new(config)
  local self = setmetatable({}, PerformanceProfiler)

  config = config or {}
  self._maxHistorySize = config.maxHistorySize or 300

  self._frameCount = 0
  self._startTime = love.timer.getTime()
  self._frameTimes = {}
  self._fpsHistory = {}
  self._memoryHistory = {}
  self._customMetrics = {}
  self._markers = {}
  self._currentFrameStart = nil
  self._lastGcCount = collectgarbage("count")
  self._snapshots = {}
  self._currentSnapshot = nil

  return self
end

---@return nil
function PerformanceProfiler:beginFrame()
  local now = love.timer.getTime()
  
  -- Calculate actual frame time from previous frame start to current frame start
  if self._currentFrameStart then
    local frameTime = (now - self._currentFrameStart) * 1000
    
    table.insert(self._frameTimes, frameTime)
    if #self._frameTimes > self._maxHistorySize then
      table.remove(self._frameTimes, 1)
    end
    
    local fps = 1000 / frameTime
    table.insert(self._fpsHistory, fps)
    if #self._fpsHistory > self._maxHistorySize then
      table.remove(self._fpsHistory, 1)
    end
    
    local memKb = collectgarbage("count")
    table.insert(self._memoryHistory, memKb / 1024)
    if #self._memoryHistory > self._maxHistorySize then
      table.remove(self._memoryHistory, 1)
    end
    
    self._lastGcCount = memKb
  end
  
  self._currentFrameStart = now
  self._frameCount = self._frameCount + 1
end

---@return nil
function PerformanceProfiler:endFrame()
  -- No longer needed - frame timing is done in beginFrame()
  -- Keeping this method for API compatibility
end

---@param name string
---@return nil
function PerformanceProfiler:markBegin(name)
  if not self._markers[name] then
    self._markers[name] = {
      times = {},
      totalTime = 0,
      count = 0,
      minTime = math.huge,
      maxTime = 0,
    }
  end

  self._markers[name].startTime = love.timer.getTime()
end

---@param name string
---@return number?
function PerformanceProfiler:markEnd(name)
  local marker = self._markers[name]
  if not marker or not marker.startTime then
    return nil
  end

  local elapsed = (love.timer.getTime() - marker.startTime) * 1000
  marker.startTime = nil

  table.insert(marker.times, elapsed)
  if #marker.times > self._maxHistorySize then
    table.remove(marker.times, 1)
  end

  marker.totalTime = marker.totalTime + elapsed
  marker.count = marker.count + 1
  marker.minTime = math.min(marker.minTime, elapsed)
  marker.maxTime = math.max(marker.maxTime, elapsed)

  return elapsed
end

---@param name string
---@param value number
---@return nil
function PerformanceProfiler:recordMetric(name, value)
  if not self._customMetrics[name] then
    self._customMetrics[name] = {
      values = {},
      total = 0,
      count = 0,
      min = math.huge,
      max = -math.huge,
    }
  end

  local metric = self._customMetrics[name]
  table.insert(metric.values, value)
  if #metric.values > self._maxHistorySize then
    table.remove(metric.values, 1)
  end

  metric.total = metric.total + value
  metric.count = metric.count + 1
  metric.min = math.min(metric.min, value)
  metric.max = math.max(metric.max, value)
end

---@param values table
---@return number
local function calculateMean(values)
  if #values == 0 then
    return 0
  end
  local sum = 0
  for _, v in ipairs(values) do
    sum = sum + v
  end
  return sum / #values
end

---@param values table
---@return number
local function calculateMedian(values)
  if #values == 0 then
    return 0
  end

  local sorted = {}
  for _, v in ipairs(values) do
    table.insert(sorted, v)
  end
  table.sort(sorted)

  local mid = math.floor(#sorted / 2) + 1
  if #sorted % 2 == 0 then
    return (sorted[mid - 1] + sorted[mid]) / 2
  else
    return sorted[mid]
  end
end

---@param values table
---@param percentile number
---@return number
local function calculatePercentile(values, percentile)
  if #values == 0 then
    return 0
  end

  local sorted = {}
  for _, v in ipairs(values) do
    table.insert(sorted, v)
  end
  table.sort(sorted)

  local index = math.ceil(#sorted * percentile / 100)
  index = math.max(1, math.min(index, #sorted))
  return sorted[index]
end

---@return table
function PerformanceProfiler:getReport()
  local now = love.timer.getTime()
  local totalTime = now - self._startTime

  local report = {
    totalTime = totalTime,
    frameCount = self._frameCount,
    averageFps = self._frameCount / totalTime,

    frameTime = {
      current = self._frameTimes[#self._frameTimes] or 0,
      average = calculateMean(self._frameTimes),
      median = calculateMedian(self._frameTimes),
      min = math.huge,
      max = 0,
      p95 = calculatePercentile(self._frameTimes, 95),
      p99 = calculatePercentile(self._frameTimes, 99),
      p99_9 = calculatePercentile(self._frameTimes, 99.9),
    },

    fps = {
      current = self._fpsHistory[#self._fpsHistory] or 0,
      average = calculateMean(self._fpsHistory),
      median = calculateMedian(self._fpsHistory),
      min = math.huge,
      max = 0,
      -- For FPS, 1% and 0.1% worst are the LOWEST values (inverse of percentile)
      worst_1_percent = calculatePercentile(self._fpsHistory, 1),
      worst_0_1_percent = calculatePercentile(self._fpsHistory, 0.1),
    },

    memory = {
      current = self._memoryHistory[#self._memoryHistory] or 0,
      average = calculateMean(self._memoryHistory),
      peak = -math.huge,
      min = math.huge,
      p95 = calculatePercentile(self._memoryHistory, 95),
      p99 = calculatePercentile(self._memoryHistory, 99),
      p99_9 = calculatePercentile(self._memoryHistory, 99.9),
    },

    markers = {},
    customMetrics = {},
  }

  -- Calculate frame time min/max
  for _, ft in ipairs(self._frameTimes) do
    report.frameTime.min = math.min(report.frameTime.min, ft)
    report.frameTime.max = math.max(report.frameTime.max, ft)
  end

  -- Calculate FPS min/max
  for _, fps in ipairs(self._fpsHistory) do
    report.fps.min = math.min(report.fps.min, fps)
    report.fps.max = math.max(report.fps.max, fps)
  end

  -- Calculate memory min/max/peak
  for _, mem in ipairs(self._memoryHistory) do
    report.memory.min = math.min(report.memory.min, mem)
    report.memory.peak = math.max(report.memory.peak, mem)
  end

  -- Add marker statistics
  for name, marker in pairs(self._markers) do
    report.markers[name] = {
      average = marker.count > 0 and (marker.totalTime / marker.count) or 0,
      median = calculateMedian(marker.times),
      min = marker.minTime,
      max = marker.maxTime,
      count = marker.count,
      p95 = calculatePercentile(marker.times, 95),
      p99 = calculatePercentile(marker.times, 99),
    }
  end

  -- Add custom metrics
  for name, metric in pairs(self._customMetrics) do
    report.customMetrics[name] = {
      average = metric.count > 0 and (metric.total / metric.count) or 0,
      median = calculateMedian(metric.values),
      min = metric.min,
      max = metric.max,
      count = metric.count,
    }
  end

  return report
end

---@param x number?
---@param y number?
---@param width number?
---@param height number?
---@return nil
function PerformanceProfiler:draw(x, y, width, height)
  x = x or 10
  y = y or 10
  width = width or 320
  height = height or 280

  local report = self:getReport()

  love.graphics.setColor(0, 0, 0, 0.85)
  love.graphics.rectangle("fill", x, y, width, height)

  love.graphics.setColor(1, 1, 1, 1)
  local lineHeight = 18
  local currentY = y + 10
  local padding = 10

  -- Title
  love.graphics.setColor(0.3, 0.8, 1, 1)
  love.graphics.print("Performance Profiler", x + padding, currentY)
  currentY = currentY + lineHeight + 5

  -- FPS
  local fpsColor = { 1, 1, 1 }
  if report.frameTime.current > 16.67 then
    fpsColor = { 1, 0, 0 }
  elseif report.frameTime.current > 13.0 then
    fpsColor = { 1, 1, 0 }
  else
    fpsColor = { 0, 1, 0 }
  end
  love.graphics.setColor(fpsColor)
  love.graphics.print(string.format("FPS: %.0f (%.2fms)", report.fps.current, report.frameTime.current), x + padding, currentY)
  currentY = currentY + lineHeight

  -- Average FPS
  love.graphics.setColor(0.8, 0.8, 0.8, 1)
  love.graphics.print(string.format("Avg: %.0f fps (%.2fms)", report.fps.average, report.frameTime.average), x + padding, currentY)
  currentY = currentY + lineHeight

  -- Frame time stats
  love.graphics.setColor(1, 1, 1, 1)
  love.graphics.print(string.format("Min/Max: %.2f/%.2fms", report.frameTime.min, report.frameTime.max), x + padding, currentY)
  currentY = currentY + lineHeight
  love.graphics.print(string.format("P95/P99: %.2f/%.2fms", report.frameTime.p95, report.frameTime.p99), x + padding, currentY)
  currentY = currentY + lineHeight + 3

  -- Memory
  love.graphics.setColor(0.5, 1, 0.5, 1)
  love.graphics.print(string.format("Memory: %.2f MB", report.memory.current), x + padding, currentY)
  currentY = currentY + lineHeight
  love.graphics.setColor(0.8, 0.8, 0.8, 1)
  love.graphics.print(string.format("Peak: %.2f MB | Avg: %.2f MB", report.memory.peak, report.memory.average), x + padding, currentY)
  currentY = currentY + lineHeight + 3

  -- Total time and frames
  love.graphics.setColor(0.7, 0.7, 1, 1)
  love.graphics.print(string.format("Frames: %d | Time: %.1fs", report.frameCount, report.totalTime), x + padding, currentY)
  currentY = currentY + lineHeight + 5

  -- Markers (top 5 by average time)
  if next(report.markers) then
    love.graphics.setColor(1, 0.8, 0.4, 1)
    love.graphics.print("Top Markers:", x + padding, currentY)
    currentY = currentY + lineHeight

    local sortedMarkers = {}
    for name, data in pairs(report.markers) do
      table.insert(sortedMarkers, { name = name, average = data.average })
    end
    table.sort(sortedMarkers, function(a, b)
      return a.average > b.average
    end)

    love.graphics.setColor(0.9, 0.9, 0.9, 1)
    for i = 1, math.min(3, #sortedMarkers) do
      local m = sortedMarkers[i]
      love.graphics.print(string.format("  %s: %.3fms", m.name, m.average), x + padding, currentY)
      currentY = currentY + lineHeight
    end
  end
end

---@return nil
function PerformanceProfiler:reset()
  self._frameCount = 0
  self._startTime = love.timer.getTime()
  self._frameTimes = {}
  self._fpsHistory = {}
  self._memoryHistory = {}
  self._customMetrics = {}
  self._markers = {}
  self._currentFrameStart = nil
  self._lastGcCount = collectgarbage("count")
  -- Don't reset snapshots - they persist across resets
end

--- Create a snapshot of current metrics with a label
---@param label string Label for this snapshot (e.g., "100 elements", "500 elements")
---@param metadata table? Additional metadata to store with snapshot
---@return nil
function PerformanceProfiler:createSnapshot(label, metadata)
  local report = self:getReport()
  
  table.insert(self._snapshots, {
    label = label,
    timestamp = os.date("%Y-%m-%d %H:%M:%S"),
    metadata = metadata or {},
    report = report,
  })
  
  -- Reset current metrics for next snapshot period
  self._frameCount = 0
  self._startTime = love.timer.getTime()
  self._frameTimes = {}
  self._fpsHistory = {}
  self._memoryHistory = {}
  self._currentFrameStart = nil
end

--- Get all snapshots
---@return table
function PerformanceProfiler:getSnapshots()
  return self._snapshots
end

--- Clear all snapshots
---@return nil
function PerformanceProfiler:clearSnapshots()
  self._snapshots = {}
end

---@return string
function PerformanceProfiler:exportJSON()
  local report = self:getReport()

  local function serializeValue(val, indent)
    indent = indent or ""
    local t = type(val)

    if t == "table" then
      local items = {}
      local isArray = true
      local count = 0

      for k, _ in pairs(val) do
        count = count + 1
        if type(k) ~= "number" or k ~= count then
          isArray = false
          break
        end
      end

      if isArray then
        for _, v in ipairs(val) do
          table.insert(items, serializeValue(v, indent .. "  "))
        end
        return "[\n" .. indent .. "  " .. table.concat(items, ",\n" .. indent .. "  ") .. "\n" .. indent .. "]"
      else
        for k, v in pairs(val) do
          table.insert(items, string.format('%s"%s": %s', indent .. "  ", k, serializeValue(v, indent .. "  ")))
        end
        return "{\n" .. table.concat(items, ",\n") .. "\n" .. indent .. "}"
      end
    elseif t == "string" then
      return string.format('"%s"', val)
    elseif t == "number" then
      if val == math.huge then
        return "null"
      elseif val == -math.huge then
        return "null"
      elseif val ~= val then -- NaN
        return "null"
      else
        return tostring(val)
      end
    elseif t == "boolean" then
      return tostring(val)
    else
      return "null"
    end
  end

  return serializeValue(report, "")
end

---@param profileName string
---@return boolean, string?
function PerformanceProfiler:saveReport(profileName)
  local report = self:getReport()
  local timestamp = os.date("%Y-%m-%d_%H-%M:%S")
  local filename = string.format("%s.md", timestamp)

  -- Get the actual project directory (where the profiling folder is)
  local sourceDir = love.filesystem.getSource()
  local profileDir, filepath
  
  if sourceDir:match("%.love$") then
    -- If running from a .love file, fall back to save directory
    sourceDir = love.filesystem.getSaveDirectory()
    profileDir = sourceDir .. "/reports/" .. profileName
    filepath = profileDir .. "/" .. filename

    -- Create profile-specific directory if it doesn't exist
    love.filesystem.createDirectory("reports/" .. profileName)
    
    return self:_saveWithLoveFS(filepath, profileName)
  else
    -- Running from source - sourceDir is the profiling directory
    -- We want to save to profiling/reports/{profileName}/
    profileDir = sourceDir .. "/reports/" .. profileName
    filepath = profileDir .. "/" .. filename

    -- Create profile-specific directory if it doesn't exist (using io module)
    os.execute('mkdir -p "' .. profileDir .. '"')

    return self:_saveWithIO(filepath, profileName)
  end
end

---@param filepath string
---@param profileName string
---@return boolean, string?
function PerformanceProfiler:_saveWithIO(filepath, profileName)
  local report = self:getReport()
  local profileDir = filepath:match("(.*/)") -- Extract directory from path

  -- Generate Markdown report
  local lines = {}
  table.insert(lines, "# Performance Profile Report: " .. profileName)
  table.insert(lines, "")
  table.insert(lines, "**Generated:** " .. os.date("%Y-%m-%d %H:%M:%S"))
  table.insert(lines, "")
  table.insert(lines, "---")
  table.insert(lines, "")

  -- Summary
  table.insert(lines, "## Summary")
  table.insert(lines, "")
  table.insert(lines, string.format("- **Total Duration:** %.2f seconds", report.totalTime))
  table.insert(lines, string.format("- **Total Frames:** %d", report.frameCount))
  table.insert(lines, "")

  -- FPS Statistics
  table.insert(lines, "## FPS Statistics")
  table.insert(lines, "")
  table.insert(lines, "| Metric | Value |")
  table.insert(lines, "|--------|-------|")
  table.insert(lines, string.format("| Average FPS | %.2f |", report.fps.average))
  table.insert(lines, string.format("| Median FPS | %.2f |", report.fps.median))
  table.insert(lines, string.format("| Min FPS | %.2f |", report.fps.min))
  table.insert(lines, string.format("| Max FPS | %.2f |", report.fps.max))
  table.insert(lines, string.format("| **1%% Worst FPS** | **%.2f** |", report.fps.worst_1_percent))
  table.insert(lines, string.format("| **0.1%% Worst FPS** | **%.2f** |", report.fps.worst_0_1_percent))
  table.insert(lines, "")
  table.insert(lines, "> 1% and 0.1% worst represent the FPS threshold at which 1% and 0.1% of frames performed at or below.")
  table.insert(lines, "")

  -- Frame Time Statistics
  table.insert(lines, "## Frame Time Statistics")
  table.insert(lines, "")
  table.insert(lines, "| Metric | Value (ms) |")
  table.insert(lines, "|--------|------------|")
  table.insert(lines, string.format("| Average | %.2f |", report.frameTime.average))
  table.insert(lines, string.format("| Median | %.2f |", report.frameTime.median))
  table.insert(lines, string.format("| Min | %.2f |", report.frameTime.min))
  table.insert(lines, string.format("| Max | %.2f |", report.frameTime.max))
  table.insert(lines, string.format("| 95th Percentile | %.2f |", report.frameTime.p95))
  table.insert(lines, string.format("| 99th Percentile | %.2f |", report.frameTime.p99))
  table.insert(lines, string.format("| 99.9th Percentile | %.2f |", report.frameTime.p99_9))
  table.insert(lines, "")

  -- Memory Statistics
  table.insert(lines, "## Memory Usage")
  table.insert(lines, "")
  table.insert(lines, "| Metric | Value (MB) |")
  table.insert(lines, "|--------|------------|")
  table.insert(lines, string.format("| Current | %.2f |", report.memory.current))
  table.insert(lines, string.format("| Average | %.2f |", report.memory.average))
  table.insert(lines, string.format("| Peak | %.2f |", report.memory.peak))
  table.insert(lines, string.format("| Min | %.2f |", report.memory.min))
  table.insert(lines, string.format("| 95th Percentile | %.2f |", report.memory.p95))
  table.insert(lines, string.format("| 99th Percentile | %.2f |", report.memory.p99))
  table.insert(lines, string.format("| 99.9th Percentile | %.2f |", report.memory.p99_9))
  table.insert(lines, "")

  -- Markers (if any)
  if next(report.markers) then
    table.insert(lines, "## Custom Markers")
    table.insert(lines, "")
    for name, data in pairs(report.markers) do
      table.insert(lines, string.format("### %s", name))
      table.insert(lines, "")
      table.insert(lines, "| Metric | Value (ms) |")
      table.insert(lines, "|--------|------------|")
      table.insert(lines, string.format("| Average | %.3f |", data.average))
      table.insert(lines, string.format("| Median | %.3f |", data.median))
      table.insert(lines, string.format("| Min | %.3f |", data.min))
      table.insert(lines, string.format("| Max | %.3f |", data.max))
      table.insert(lines, string.format("| Count | %d |", data.count))
      table.insert(lines, "")
    end
  end

  -- Custom Metrics (if any)
  if next(report.customMetrics) then
    table.insert(lines, "## Custom Metrics")
    table.insert(lines, "")
    for name, data in pairs(report.customMetrics) do
      table.insert(lines, string.format("### %s", name))
      table.insert(lines, "")
      table.insert(lines, "| Metric | Value |")
      table.insert(lines, "|--------|-------|")
      table.insert(lines, string.format("| Average | %.2f |", data.average))
      table.insert(lines, string.format("| Median | %.2f |", data.median))
      table.insert(lines, string.format("| Min | %.2f |", data.min))
      table.insert(lines, string.format("| Max | %.2f |", data.max))
      table.insert(lines, string.format("| Count | %d |", data.count))
      table.insert(lines, "")
    end
  end

  -- Snapshots (if any)
  if #self._snapshots > 0 then
    table.insert(lines, "## Snapshots")
    table.insert(lines, "")
    table.insert(lines, "> Performance metrics captured at different configuration points")
    table.insert(lines, "")
    
    for i, snapshot in ipairs(self._snapshots) do
      table.insert(lines, string.format("### %d. %s", i, snapshot.label))
      table.insert(lines, "")
      table.insert(lines, string.format("**Captured:** %s", snapshot.timestamp))
      
      -- Show metadata if present
      if next(snapshot.metadata) then
        table.insert(lines, "")
        table.insert(lines, "**Configuration:**")
        for key, value in pairs(snapshot.metadata) do
          table.insert(lines, string.format("- %s: `%s`", key, tostring(value)))
        end
      end
      
      table.insert(lines, "")
      
      local r = snapshot.report
      
      -- Compact FPS/Frame Time table
      table.insert(lines, "| FPS | Frame Time (ms) | Memory (MB) | Frames |")
      table.insert(lines, "|-----|-----------------|-------------|--------|")
      table.insert(lines, string.format("| Avg: %.1f | Avg: %.2f | Avg: %.2f | %d |",
        r.fps.average, r.frameTime.average, r.memory.average, r.frameCount))
      table.insert(lines, string.format("| 1%% Worst: **%.1f** | P99: %.2f | Peak: %.2f | Duration: %.1fs |",
        r.fps.worst_1_percent, r.frameTime.p99, r.memory.peak, r.totalTime))
      table.insert(lines, "")
    end
    
    table.insert(lines, "---")
    table.insert(lines, "")
  end

  table.insert(lines, "---")
  table.insert(lines, "")

  -- Save to file using io module (writes to actual directory, not sandboxed)
  local content = table.concat(lines, "\n")
  local file, err = io.open(filepath, "w")

  if not file then
    return false, "Failed to open file: " .. tostring(err)
  end

  local success, writeErr = pcall(function()
    file:write(content)
    file:close()
  end)

  if not success then
    return false, "Failed to write report: " .. tostring(writeErr)
  end

  -- Also save JSON version
  local jsonFilepath = filepath:gsub("%.md$", ".json")
  local jsonFile = io.open(jsonFilepath, "w")
  if jsonFile then
    pcall(function()
      jsonFile:write(self:exportJSON())
      jsonFile:close()
    end)
  end

  -- Save as "latest" for easy access
  local latestMarkdownPath = profileDir .. "/latest.md"
  local latestJsonPath = profileDir .. "/latest.json"
  
  local latestMdFile = io.open(latestMarkdownPath, "w")
  if latestMdFile then
    pcall(function()
      latestMdFile:write(content)
      latestMdFile:close()
    end)
  end
  
  local latestJsonFile = io.open(latestJsonPath, "w")
  if latestJsonFile then
    pcall(function()
      latestJsonFile:write(self:exportJSON())
      latestJsonFile:close()
    end)
  end

  return true, filepath
end

return PerformanceProfiler
