--[[
  WebPExportServiceProvider.lua
  WebP Exporter (cwebp) – v1.2.0
  Author: Marco De Maio
  https://www.marcodemaio.it/webp-lightroom-classic-export-plugin/

  Changes in 1.2.0:
  - Separated UI into ExportDialog.lua
  - Windows: cmd.exe /c wrapper for reliable shell execution
  - Windows: robust quoting for paths with spaces and unicode chars
  - Output validation based on file existence (not only return code)
  - resolveCwebp improved: tracks discovery source for logging
  - Full diagnostics log written to export folder (webp-export-log.txt)
  - Debug mode toggle (verbose per-photo log entries)
  - Keep/delete intermediate TIFF explicit option
  - Test button in UI (via ExportDialog.lua)
--]]

local LrDialogs     = import 'LrDialogs'
local LrTasks       = import 'LrTasks'
local LrPathUtils   = import 'LrPathUtils'
local LrFileUtils   = import 'LrFileUtils'
local LrLogger      = import 'LrLogger'
local LrApplication = import 'LrApplication'

local ExportDialog  = require 'ExportDialog'

local logger = LrLogger('WebPExport')
logger:enable('logfile')

-- ── Constants ────────────────────────────────────────────────────────────────

local PLUGIN_VERSION = "1.2.2"

-- ── Platform helpers ─────────────────────────────────────────────────────────

local function isWin() return WIN_ENV end

local function quotePosix(s)
  if s == nil then return "''" end
  return "'" .. string.gsub(s, "'", "'\"'\"'") .. "'"
end

local function quoteWin(s)
  if s == nil then return '""' end
  s = string.gsub(s, '"', '""')
  return '"' .. s .. '"'
end

local function trim(s)
  if not s then return "" end
  return (s:gsub("^%s+", ""):gsub("%s+$", ""))
end

local function normalizeExePath(p)
  if not p then return "" end
  p = trim(p)
  if (#p >= 2) and (
    (p:sub(1,1) == '"' and p:sub(-1) == '"') or
    (p:sub(1,1) == "'" and p:sub(-1) == "'")
  ) then
    p = p:sub(2, -2)
    p = trim(p)
  end
  return p
end

local function fileExists(path)
  return path and path ~= '' and LrFileUtils.exists(path)
end

-- ── Logging ──────────────────────────────────────────────────────────────────

local logFilePath = nil   -- set at export start

local function initLog(exportFolder, debugMode)
  logFilePath = LrPathUtils.child(exportFolder, "webp-export-log.txt")
  local f = io.open(logFilePath, "w")
  if not f then
    logFilePath = nil
    logger:warn("Cannot open log file in: " .. tostring(exportFolder))
    return
  end

  local osName
  if isWin() then
    osName = "Windows"
  else
    local arch = "unknown"
    pcall(function()
      local h = io.popen("uname -m 2>/dev/null")
      if h then
        local r = trim(h:read("*l") or "")
        h:close()
        if r ~= "" then arch = r end
      end
    end)
    osName = "macOS (" .. arch .. ")"
  end
  local lrVer  = "unknown"
  pcall(function()
    local v = LrApplication.versionTable()
    lrVer = string.format("%d.%d.%d", v.major or 0, v.minor or 0, v.revision or 0)
  end)

  f:write("============================================================\n")
  f:write("  WebP Exporter (cwebp) – Export Log\n")
  f:write("============================================================\n")
  f:write(string.format("  Plugin version : %s\n", PLUGIN_VERSION))
  f:write(string.format("  Date / Time    : %s\n", os.date("%Y-%m-%d %H:%M:%S")))
  f:write(string.format("  OS             : %s\n", osName))
  f:write(string.format("  Lightroom ver  : %s\n", lrVer))
  f:write(string.format("  Export folder  : %s\n", tostring(exportFolder)))
  f:write(string.format("  Debug mode     : %s\n", tostring(debugMode)))
  f:write("============================================================\n\n")
  f:close()
end

local function writeLog(msg)
  if not logFilePath then return end
  local f = io.open(logFilePath, "a")
  if not f then return end
  f:write("[" .. os.date("%H:%M:%S") .. "] " .. tostring(msg) .. "\n")
  f:close()
  logger:trace(msg)
end

local function writeSeparator()
  if not logFilePath then return end
  local f = io.open(logFilePath, "a")
  if not f then return end
  f:write("------------------------------------------------------------\n")
  f:close()
end

local function writeSummary(total, success, failures, skipped)
  if not logFilePath then return end
  local f = io.open(logFilePath, "a")
  if not f then return end
  f:write("\n============================================================\n")
  f:write("  EXPORT SUMMARY\n")
  f:write("============================================================\n")
  f:write(string.format("  Total photos   : %d\n", total))
  f:write(string.format("  Succeeded      : %d\n", success))
  f:write(string.format("  Failed         : %d\n", failures))
  f:write(string.format("  Skipped/cancel : %d\n", skipped))
  f:write(string.format("  Completed at   : %s\n", os.date("%Y-%m-%d %H:%M:%S")))
  f:write("============================================================\n")
  f:close()
end

-- ── cwebp resolution ─────────────────────────────────────────────────────────

local function validateExe(exePath)
  exePath = normalizeExePath(exePath)
  if exePath == "" then return false end
  local cmd
  if isWin() then
    cmd = 'cmd.exe /c "' .. exePath .. '" -version > NUL 2>&1'
  else
    cmd = quotePosix(exePath) .. ' -version > /dev/null 2>&1'
  end
  local rc = LrTasks.execute(cmd)
  return rc == 0
end

-- Detect macOS CPU architecture: returns "arm64" or "x86_64"
local function macArch()
  -- uname -m returns "arm64" on Apple Silicon, "x86_64" on Intel
  local handle = io.popen("uname -m 2>/dev/null")
  if not handle then return "x86_64" end
  local result = trim(handle:read("*l") or "")
  handle:close()
  if result == "arm64" then return "arm64" end
  return "x86_64"
end

local function resolveCwebp(settings)
  local candidates = {}

  if settings.cwebpPath and settings.cwebpPath ~= "" then
    table.insert(candidates, { path = normalizeExePath(settings.cwebpPath), source = "user setting" })
  end

  local binDir = LrPathUtils.child(_PLUGIN.path, "bin")
  if isWin() then
    table.insert(candidates, { path = LrPathUtils.child(LrPathUtils.child(binDir, "win"), "cwebp.exe"), source = "plugin bundle (win)" })
    table.insert(candidates, { path = LrPathUtils.child(binDir, "cwebp.exe"),                          source = "plugin bundle root" })
    table.insert(candidates, { path = "C:\\Program Files\\WebP\\bin\\cwebp.exe",                       source = "Program Files" })
    table.insert(candidates, { path = "C:\\Program Files (x86)\\WebP\\bin\\cwebp.exe",                 source = "Program Files (x86)" })
  else
    -- Detect architecture and prioritize the matching binary
    local arch = macArch()
    local macBinDir = LrPathUtils.child(binDir, "mac")
    local archDir   = LrPathUtils.child(macBinDir, arch)
    local otherDir  = LrPathUtils.child(macBinDir, arch == "arm64" and "x86_64" or "arm64")

    -- 1. Architecture-specific bundle (e.g. bin/mac/arm64/cwebp)
    table.insert(candidates, { path = LrPathUtils.child(archDir,   "cwebp"), source = "plugin bundle mac/" .. arch })
    -- 2. Other architecture as fallback (Rosetta 2 runs x86_64 on arm64)
    table.insert(candidates, { path = LrPathUtils.child(otherDir,  "cwebp"), source = "plugin bundle mac/" .. (arch == "arm64" and "x86_64" or "arm64") .. " (Rosetta)" })
    -- 3. Generic mac bundle (bin/mac/cwebp) for universal or manually placed binaries
    table.insert(candidates, { path = LrPathUtils.child(macBinDir, "cwebp"), source = "plugin bundle mac (generic)" })
    -- 4. Plugin root bin/ fallback
    table.insert(candidates, { path = LrPathUtils.child(binDir,    "cwebp"), source = "plugin bundle root" })
    -- 5. System paths
    table.insert(candidates, { path = "/opt/homebrew/bin/cwebp",              source = "Homebrew Apple Silicon" })
    table.insert(candidates, { path = "/usr/local/bin/cwebp",                 source = "Homebrew Intel" })
    table.insert(candidates, { path = "/usr/bin/cwebp",                       source = "/usr/bin" })
    table.insert(candidates, { path = "/bin/cwebp",                           source = "/bin" })
  end

  writeLog("Searching for cwebp executable...")

  for _, entry in ipairs(candidates) do
    writeLog(string.format("  Checking: %s  [%s]", entry.path, entry.source))
    if fileExists(entry.path) then
      writeLog("    → file exists, validating...")
      if validateExe(entry.path) then
        writeLog("    → OK, will use this executable")
        return entry.path, entry.source
      else
        writeLog("    → validation failed (not executable or wrong binary)")
      end
    else
      writeLog("    → not found")
    end
  end

  -- PATH fallback
  if isWin() then
    writeLog("  Checking: cwebp  [system PATH via where.exe]")
    local rc = LrTasks.execute('cmd.exe /c where cwebp > NUL 2>&1')
    if rc == 0 then
      writeLog("    → found in PATH")
      return "cwebp", "system PATH"
    else
      writeLog("    → not in PATH")
    end
  else
    writeLog("  Checking: cwebp  [system PATH]")
    local rc = LrTasks.execute('cwebp -version > /dev/null 2>&1')
    if rc == 0 then
      writeLog("    → found in PATH")
      return "cwebp", "system PATH"
    else
      writeLog("    → not in PATH")
    end
  end

  writeLog("ERROR: cwebp not found in any location.")
  return nil, "not found"
end

-- ── Command building ─────────────────────────────────────────────────────────

local function replaceExtension(path, newExt)
  local dot = string.match(path, "^.*()%.")
  if dot then
    return string.sub(path, 1, dot - 1) .. "." .. newExt
  end
  return path .. "." .. newExt
end

local function buildCwebpArgs(settings, inputPath, outputPath)
  local args = {}

  -- Preset
  if settings.webpPreset and settings.webpPreset ~= 'default' then
    table.insert(args, "-preset")
    table.insert(args, settings.webpPreset)
  end

  -- Mode / quality
  local mode = settings.webpMode or "lossy"
  local q    = math.max(0, math.min(100, tonumber(settings.webpQuality) or 80))

  if mode == "lossless" then
    table.insert(args, "-lossless")
    table.insert(args, "-q"); table.insert(args, string.format("%.0f", q))
    local z = math.max(0, math.min(9, tonumber(settings.webpZLevel) or 6))
    table.insert(args, "-z"); table.insert(args, tostring(z))
  elseif mode == "near_lossless" then
    local nl = math.max(0, math.min(100, tonumber(settings.webpNearLossless) or 60))
    table.insert(args, "-near_lossless"); table.insert(args, tostring(nl))
    table.insert(args, "-q");             table.insert(args, string.format("%.0f", q))
  else
    table.insert(args, "-q"); table.insert(args, string.format("%.0f", q))
  end

  -- Method
  local m = math.max(0, math.min(6, tonumber(settings.webpMethod) or 4))
  table.insert(args, "-m"); table.insert(args, tostring(m))

  -- Multi-thread
  if settings.webpUseMt then table.insert(args, "-mt") end

  -- Metadata
  local md = settings.webpMetadata or "none"
  if md and md ~= "" then
    table.insert(args, "-metadata"); table.insert(args, md)
  end

  -- Quiet (suppress cwebp output – we handle errors ourselves)
  table.insert(args, "-quiet")

  -- Input / output (platform-aware quoting)
  if isWin() then
    table.insert(args, quoteWin(inputPath))
    table.insert(args, "-o"); table.insert(args, quoteWin(outputPath))
  else
    table.insert(args, quotePosix(inputPath))
    table.insert(args, "-o"); table.insert(args, quotePosix(outputPath))
  end

  return table.concat(args, " ")
end

local function buildCommand(exe, argsString)
  exe = normalizeExePath(exe)
  if exe == "cwebp" then
    -- PATH fallback: let the shell find it
    if isWin() then
      return 'cmd.exe /c cwebp ' .. argsString
    else
      return 'cwebp ' .. argsString
    end
  end

  if isWin() then
    -- Wrap with cmd.exe /c for reliable execution on Windows
    -- The outer quotes are required when the exe path itself contains spaces
    return 'cmd.exe /c "' .. quoteWin(exe) .. ' ' .. argsString .. '"'
  else
    return quotePosix(exe) .. ' ' .. argsString
  end
end

-- ── Error dialog ─────────────────────────────────────────────────────────────

local function showConfigError(logPath)
  local logNote = logPath
    and ("\n\nA log file was created at:\n" .. logPath .. "\nSend it to the developer if you need help.")
    or ""

  LrDialogs.message(
    "WebP Exporter – cwebp Not Found",
    "cwebp was not found on this system.\n\n" ..

    "macOS:\n" ..
    "  • Install with Homebrew:  brew install webp\n" ..
    "  • Common paths:\n" ..
    "      /opt/homebrew/bin/cwebp   (Apple Silicon)\n" ..
    "      /usr/local/bin/cwebp      (Intel)\n" ..
    "  • Or copy cwebp into:  LightroomWebPExport.lrplugin/bin/mac/cwebp\n" ..
    "    then run:  chmod +x /path/to/cwebp\n" ..
    "    If macOS blocks it:  xattr -d com.apple.quarantine /path/to/cwebp\n\n" ..

    "Windows:\n" ..
    "  • Download libwebp from:\n" ..
    "      https://developers.google.com/speed/webp/docs/precompiled\n" ..
    "  • Copy cwebp.exe into:  LightroomWebPExport.lrplugin\\bin\\win\\cwebp.exe\n" ..
    "  • Or set the full path in Export > WebP Settings > cwebp executable path\n" ..
    "    e.g.  C:\\Tools\\WebP\\bin\\cwebp.exe\n\n" ..

    "Use the '▶ Test cwebp' button in the export panel to verify the setup." ..
    logNote,
    "critical"
  )
end

-- ── Service provider ─────────────────────────────────────────────────────────

local WebP = {}
WebP.hideSections = { "fileSettings", "video" }

WebP.exportPresetFields       = ExportDialog.exportPresetFields()
WebP.startDialog              = ExportDialog.startDialog
WebP.sectionsForBottomOfDialog = ExportDialog.sectionsForBottomOfDialog

function WebP.updateExportSettings(exportSettings)
  exportSettings.LR_format               = "TIFF"
  exportSettings.LR_export_bitDepth      = 8
  exportSettings.LR_tiff_compressionMethod = "none"
  exportSettings.LR_export_addToCatalog  = false

  if exportSettings.webpColorSpace and exportSettings.webpColorSpace ~= "" then
    exportSettings.LR_export_colorSpace = exportSettings.webpColorSpace
  else
    exportSettings.LR_export_colorSpace = "sRGB"
  end
end

function WebP.processRenderedPhotos(functionContext, exportContext)
  local exportSettings = exportContext.propertyTable
  local exportSession  = exportContext.exportSession
  local nRenditions    = exportSession:countRenditions()
  local debugMode      = exportSettings.debugMode == true

  -- ── Determine export folder for the log ─────────────────────────────────
  -- We can't know the export folder before rendering starts.
  -- We use the temp folder initially and update it from the first rendition.
  local exportFolder = LrPathUtils.getStandardFilePath('temp')
  local logInitialized = false

  local function ensureLog(folder)
    if not logInitialized then
      exportFolder = folder
      initLog(exportFolder, debugMode)
      logInitialized = true
      writeLog(string.format("Export started – %d photo(s) queued", nRenditions))
    end
  end

  -- ── Resolve cwebp ────────────────────────────────────────────────────────
  local exe, exeSource = resolveCwebp(exportSettings)

  if not exe then
    initLog(exportFolder, debugMode)
    writeLog("ERROR: cwebp not found. Export aborted.")
    writeSummary(nRenditions, 0, nRenditions, 0)
    showConfigError(logFilePath)
    return
  end

  writeLog(string.format("cwebp resolved: %s  (source: %s)", exe, exeSource))
  writeSeparator()

  -- ── Progress ─────────────────────────────────────────────────────────────
  local progressScope = exportContext:configureProgress {
    title   = "Exporting WebP (cwebp)",
    caption = "Converting photos…",
  }
  progressScope:setCancelable(true)

  local success      = 0
  local failures     = 0
  local skipped      = 0
  local i            = 0
  local totalWebpSize = 0

  for _, rendition in exportContext:renditions { stopIfCanceled = true } do
    i = i + 1
    progressScope:setPortionComplete(i - 1, nRenditions)
    progressScope:setCaption(string.format("Converting %d / %d…", i, nRenditions))

    if progressScope:isCanceled() then
      writeLog(string.format("Photo %d/%d – CANCELED by user", i, nRenditions))
      rendition:renditionIsDone(false, "Canceled by user")
      skipped = skipped + 1
      break
    end

    -- Wait for Lightroom to finish rendering the intermediate TIFF
    local ok, pathOrMessage = rendition:waitForRender()

    if not ok then
      failures = failures + 1
      local errMsg = tostring(pathOrMessage)
      writeLog(string.format("Photo %d/%d – RENDER FAILED: %s", i, nRenditions, errMsg))
      rendition:renditionIsDone(false, "Lightroom render failed: " .. errMsg)

    else
      local renderedPath = rendition.destinationPath or pathOrMessage
      local webpPath     = replaceExtension(renderedPath, "webp")

      -- Init log on first successful render using the real export folder
      ensureLog(LrPathUtils.parent(renderedPath))

      if debugMode then
        writeLog(string.format("Photo %d/%d", i, nRenditions))
        writeLog("  Input  : " .. tostring(renderedPath))
        writeLog("  Output : " .. tostring(webpPath))
        writeLog("  cwebp  : " .. tostring(exe))
      end

      -- Build and run command
      local args = buildCwebpArgs(exportSettings, renderedPath, webpPath)
      local cmd  = buildCommand(exe, args)

      if debugMode then
        writeLog("  Command: " .. cmd)
      end

      local rc = LrTasks.execute(cmd)

      -- ── Validate output ─────────────────────────────────────────────────
      -- Primary check: does the .webp file exist and has a non-zero size?
      local webpExistsRaw = LrFileUtils.exists(webpPath)
      local webpExists    = webpExistsRaw and webpExistsRaw ~= false
      local webpSize      = 0
      if webpExists then
        pcall(function()
          local attr = LrFileUtils.fileAttributes(webpPath)
          if attr then webpSize = attr.fileSize or 0 end
        end)
      end

      local conversionOk = webpExists and webpSize > 0

      if debugMode then
        writeLog(string.format("  Return code : %s", tostring(rc)))
        writeLog(string.format("  WebP exists : %s", webpExists and "true" or "false"))
        writeLog(string.format("  WebP size   : %d bytes", webpSize))
        writeLog(string.format("  Result      : %s", conversionOk and "SUCCESS" or "FAILURE"))
      end

      if not conversionOk then
        failures = failures + 1
        local reason
        if not webpExists then
          reason = string.format("WebP file not created (cwebp rc=%s). Check log.", tostring(rc))
        else
          reason = string.format("WebP file is empty (cwebp rc=%s). Possible encoding error.", tostring(rc))
        end
        writeLog(string.format("Photo %d/%d – FAILED: %s", i, nRenditions, reason))
        logger:error(string.format("cwebp failed rc=%s in=%s", tostring(rc), tostring(renderedPath)))
        rendition:renditionIsDone(false, reason)

      else
        -- Delete intermediate TIFF if requested
        if exportSettings.deleteIntermediate then
          local delOk, delErr = pcall(function() LrFileUtils.delete(renderedPath) end)
          if debugMode then
            writeLog("  Intermediate TIFF deleted: " .. tostring(delOk))
            if not delOk then writeLog("  Delete error: " .. tostring(delErr)) end
          end
        else
          if debugMode then
            writeLog("  Intermediate TIFF kept (deleteIntermediate=false)")
          end
        end

        success = success + 1
        totalWebpSize = totalWebpSize + webpSize
        if not debugMode then
          -- Always log successes at brief level
          writeLog(string.format("Photo %d/%d – OK  → %s", i, nRenditions, LrPathUtils.leafName(webpPath)))
        else
          writeLog(string.format("Photo %d/%d – OK", i, nRenditions))
        end

        rendition:renditionIsDone(true)
      end
    end -- ok

    writeSeparator()
  end -- renditions loop

  progressScope:done()

  writeSummary(nRenditions, success, failures, skipped)

  -- ── Final notification ───────────────────────────────────────────────────
  local logNote = logFilePath
    and ("\n\nDiagnostic log:\n" .. logFilePath)
    or ""

  -- Format total size in KB or MB
  local sizeStr
  if totalWebpSize >= 1048576 then
    sizeStr = string.format("%.2f MB", totalWebpSize / 1048576)
  else
    sizeStr = string.format("%.0f KB", totalWebpSize / 1024)
  end

  if failures == 0 and success > 0 then
    -- Full success
    local msg = string.format(
      "Export completed successfully!\n\n" ..
      "Photos exported : %d\n" ..
      "Total size      : %s\n" ..
      "Format          : WebP\n" ..
      "Folder          : %s",
      success,
      sizeStr,
      tostring(exportFolder)
    )
    if skipped > 0 then
      msg = msg .. string.format("\nSkipped         : %d", skipped)
    end
    LrDialogs.message("WebP Exporter", msg, "info")

  elseif failures > 0 then
    -- Partial or full failure
    LrDialogs.message(
      "WebP Exporter",
      string.format(
        "Export completed with %d error(s).\n\n" ..
        "Successful : %d\n" ..
        "Failed     : %d\n" ..
        "Skipped    : %d\n\n" ..
        "Enable 'Debug mode' in export settings and re-export\n" ..
        "to get a detailed log file in the export folder." ..
        logNote,
        failures, success, failures, skipped
      ),
      "warning"
    )
  end
end

return WebP
