local SaveData = require("src.core.SaveData")
local Version = require("src.core.Version")

local IssueReport = {}

local FORM_URL = "https://github.com/bryanthaboi/gen1recomp/issues/new"
local TEMPLATE = "bug_report.yml"

local APPLE_MODELS = {
  ["iPhone14,2"] = "iPhone 13 Pro",
  ["iPhone14,3"] = "iPhone 13 Pro Max",
  ["iPhone14,4"] = "iPhone 13 mini",
  ["iPhone14,5"] = "iPhone 13",
  ["iPhone14,7"] = "iPhone 14",
  ["iPhone14,8"] = "iPhone 14 Plus",
  ["iPhone15,2"] = "iPhone 14 Pro",
  ["iPhone15,3"] = "iPhone 14 Pro Max",
  ["iPhone15,4"] = "iPhone 15",
  ["iPhone15,5"] = "iPhone 15 Plus",
  ["iPhone16,1"] = "iPhone 15 Pro",
  ["iPhone16,2"] = "iPhone 15 Pro Max",
  ["iPhone17,1"] = "iPhone 16 Pro",
  ["iPhone17,2"] = "iPhone 16 Pro Max",
  ["iPhone17,3"] = "iPhone 16",
  ["iPhone17,4"] = "iPhone 16 Plus",
  ["iPhone17,5"] = "iPhone 16e",
  ["Mac14,2"] = "MacBook Air (13-inch, M2)",
  ["Mac14,3"] = "Mac mini (M2)",
  ["Mac14,5"] = "MacBook Pro (14-inch, M2 Max)",
  ["Mac14,6"] = "MacBook Pro (16-inch, M2 Max)",
  ["Mac14,7"] = "MacBook Pro (13-inch, M2)",
  ["Mac14,9"] = "MacBook Pro (14-inch, M2 Pro)",
  ["Mac14,10"] = "MacBook Pro (16-inch, M2 Pro)",
  ["Mac14,12"] = "Mac mini (M2 Pro)",
  ["Mac14,13"] = "Mac Studio (M2 Max)",
  ["Mac14,14"] = "Mac Studio (M2 Ultra)",
  ["Mac14,15"] = "MacBook Air (15-inch, M2)",
  ["Mac15,3"] = "MacBook Pro (14-inch, M3)",
  ["Mac15,6"] = "MacBook Pro (14-inch, M3 Pro)",
  ["Mac15,7"] = "MacBook Pro (16-inch, M3 Pro)",
  ["Mac15,12"] = "MacBook Air (13-inch, M3)",
  ["Mac15,13"] = "MacBook Air (15-inch, M3)",
  ["Mac16,1"] = "MacBook Pro (14-inch, M4)",
  ["Mac16,5"] = "MacBook Pro (16-inch, M4 Max)",
  ["Mac16,6"] = "MacBook Pro (14-inch, M4 Max)",
  ["Mac16,7"] = "MacBook Pro (16-inch, M4 Pro)",
  ["Mac16,8"] = "MacBook Pro (14-inch, M4 Pro)",
  ["Mac16,10"] = "Mac mini (M4)",
  ["Mac16,11"] = "Mac mini (M4 Pro)",
}

local function clean(value)
  if value == nil then return nil end
  local text = tostring(value):gsub("^%s+", ""):gsub("%s+$", "")
  if text == "" or text == "unknown" or text == "Unknown" then return nil end
  return text
end

local function call(fn, ...)
  if type(fn) ~= "function" then return nil end
  local ok, a, b, c, d, e = pcall(fn, ...)
  if not ok then return nil end
  return a, b, c, d, e
end

local function invoke(fn, ...)
  if type(fn) ~= "function" then return false end
  local ok, result = pcall(fn, ...)
  return ok, result
end

local function commandValue(command)
  if not io or type(io.popen) ~= "function" then return nil end
  local ok, pipe = pcall(io.popen, command, "r")
  if not ok or not pipe then return nil end
  local readOK, value = pcall(pipe.read, pipe, "*l")
  pcall(pipe.close, pipe)
  if not readOK then return nil end
  return clean(value)
end

local function commandText(command)
  if not io or type(io.popen) ~= "function" then return nil end
  local ok, pipe = pcall(io.popen, command, "r")
  if not ok or not pipe then return nil end
  local readOK, value = pcall(pipe.read, pipe, "*a")
  pcall(pipe.close, pipe)
  if not readOK then return nil end
  return clean(value)
end

local function percentEncode(value)
  local text = tostring(value or "")
  return (text:gsub("([^%w%-_%.~])", function(char)
    return ("%%%02X"):format(char:byte())
  end))
end

local function formOS(raw)
  local values = {
    ["OS X"] = "macOS",
    macOS = "macOS",
    Windows = "Windows",
    Linux = "Linux",
    Android = "Android",
    iOS = "iOS",
    NX = "Nintendo Switch",
    UWP = "Xbox",
    Xbox = "Xbox",
  }
  return values[raw] or ""
end

local function loveVersion()
  local major, minor, revision, codename = call(love and love.getVersion)
  if not major then return "" end
  local result = tostring(major) .. "." .. tostring(minor) .. "." .. tostring(revision)
  if codename and codename ~= "" then result = result .. " (" .. tostring(codename) .. ")" end
  return result
end

local function friendlyModel(identifier)
  identifier = clean(identifier)
  if not identifier then return nil end
  return APPLE_MODELS[identifier] or identifier
end

local function macModel()
  local details = commandText("system_profiler SPHardwareDataType 2>/dev/null")
  if details then
    local name = clean(details:match("Model Name:%s*([^\r\n]+)"))
    local chip = clean(details:match("Chip:%s*([^\r\n]+)"))
    if name and chip and not name:find(chip, 1, true) then
      return name .. " (" .. chip .. ")"
    end
    if name then return name end
  end
  return friendlyModel(commandValue("sysctl -n hw.model 2>/dev/null"))
end

local function appVersion()
  local version = clean(Version.engine)
  if not version or version == "0.0.0" or version == "0.0.0-dev" then return "" end
  return version
end

local function deviceModel(rawOS, system)
  local nativeModel = clean(call(system.getDeviceModel))
  if nativeModel then return friendlyModel(nativeModel) end
  if rawOS == "OS X" or rawOS == "macOS" then
    return macModel()
  end
  local model = clean(call(system.getModel))
  if model and not model:lower():find("gpu", 1, true)
      and not model:lower():find("renderer", 1, true) then
    return friendlyModel(model)
  end
  if rawOS == "Windows" then
    return friendlyModel(commandValue("powershell.exe -NoProfile -NonInteractive -Command \"(Get-CimInstance Win32_ComputerSystem).Model\" 2>NUL"))
  end
  if rawOS == "Linux" then
    return friendlyModel(commandValue("cat /sys/devices/virtual/dmi/id/product_name 2>/dev/null")
      or commandValue("cat /sys/devices/virtual/dmi/id/model 2>/dev/null"))
  end
  if rawOS == "Android" then
    return friendlyModel(commandValue("getprop ro.product.model 2>/dev/null"))
  end
  return nil
end

local function modRows(context)
  if context and type(context.mods) == "table" then return context.mods end
  local ok, LauncherMods = pcall(require, "src.mods.LauncherMods")
  if ok and LauncherMods and LauncherMods.list then
    local listed = call(LauncherMods.list)
    if type(listed) == "table" then return listed end
  end
  return {}
end

local function modNames(rows, safeMode)
  local enabled = {}
  for _, mod in ipairs(rows or {}) do
    if type(mod) == "table" then
      local name = clean(mod.name or mod.id)
      if name and not safeMode and mod.enabled == true then
        enabled[#enabled + 1] = name
      end
    end
  end
  table.sort(enabled)
  return enabled
end

local function metadata(options, context)
  local system = love and love.system or {}
  local graphics = love and love.graphics or {}
  local window = love and love.window or {}
  local rawOS = clean(call(system.getOS))
  local model = deviceModel(rawOS, system)
  local renderer, rendererVersion = call(graphics.getRendererInfo)
  local width, height = call(graphics.getDimensions)
  local pixelWidth, pixelHeight = call(graphics.getPixelDimensions)
  local modeWidth, modeHeight, flags = call(window.getMode)
  local safeMode = SaveData.isSafeMode(options)
  local rows = modRows(context or {})
  local enabledMods = modNames(rows, safeMode)
  local lines = { "Diagnostics:" }
  local function add(label, value)
    value = clean(value)
    if value then lines[#lines + 1] = "- " .. label .. ": " .. value end
  end
  add("Platform", formOS(rawOS))
  add("Device", model)
  local rendererDetails = clean(renderer)
  if rendererDetails and clean(rendererVersion) then
    rendererDetails = rendererDetails .. " " .. clean(rendererVersion)
  end
  add("Renderer", rendererDetails)
  local displayWidth, displayHeight = width or modeWidth or pixelWidth, height or modeHeight or pixelHeight
  if displayWidth and displayHeight then
    add("Display", tostring(displayWidth) .. "x" .. tostring(displayHeight))
  end
  if pixelWidth and pixelHeight
      and (pixelWidth ~= displayWidth or pixelHeight ~= displayHeight) then
    add("Pixel display", tostring(pixelWidth) .. "x" .. tostring(pixelHeight))
  end
  if flags and flags.fullscreen == true then add("Fullscreen", "yes") end
  local version = appVersion()
  -- Bug reports always want the stamped engine when we have one; window
  -- chrome hides it on release builds (Version.title).  Unstamped
  -- working-tree builds stay as plain "gen1recomp" so the placeholder
  -- never lands in a filed issue.
  add("App", version ~= "" and ("gen1recomp v" .. version) or "gen1recomp")
  add("LÖVE", loveVersion())
  if safeMode then add("Safe mode", "on") end
  return {
    rawOS = rawOS,
    os = formOS(rawOS),
    device = model,
    version = version,
    safeMode = safeMode,
    enabledMods = enabledMods,
    metadata = table.concat(lines, "\n"),
  }
end

function IssueReport.build(options, context)
  options = options or SaveData.loadOptions()
  context = context or {}
  local info = metadata(options, context)
  local fields = {
    summary = "",
    mods_which = #info.enabledMods > 0 and table.concat(info.enabledMods, ", ") or "",
    version = info.version or "",
    location = "",
    screenshot = "",
    steps = "",
    expected = "",
    extra = info.metadata,
  }
  local params = {
    "template=" .. percentEncode(TEMPLATE),
    "title=" .. percentEncode("bug: replace this with a meaningful title"),
  }
  local order = { "summary", "mods_which",
    "version", "location", "screenshot", "steps", "expected", "extra" }
  for _, key in ipairs(order) do
    params[#params + 1] = key .. "=" .. percentEncode(fields[key])
  end
  return FORM_URL .. "?" .. table.concat(params, "&"), fields, info
end

function IssueReport.open(options, context)
  local url = IssueReport.build(options, context)
  local system = love and love.system or {}
  local opened, openResult = invoke(system.openURL, url)
  if opened and openResult ~= false then
    return true, url
  end
  local copied, copyResult = invoke(system.setClipboardText, url)
  if copied and copyResult ~= false then
    return true, url, "Issue URL copied to the clipboard."
  end
  local filesystem = love and love.filesystem or {}
  local written, writeResult = invoke(filesystem.write, "issue-report-url.txt", url)
  if written and writeResult ~= false then
    return true, url, "Issue URL saved to issue-report-url.txt."
  end
  return false, url, "No browser, clipboard, or writable save directory is available for the issue report."
end

IssueReport.percentEncode = percentEncode
IssueReport.metadata = metadata

return IssueReport
