--[[
    Commander 3.0 - VFS SavedVariables Driver

    Storage backend for virtual filesystem.
    - Separates Metadata (Inode) from Content (Block).
    - Single flat data table for all file content.
    - O(1) metadata access for directory listings.
]]

local ADDON_NAME, C3 = ...

local SavedVarsDriver = C3.class()
C3.VFSSavedVarsDriver = SavedVarsDriver

-- Configuration
SavedVarsDriver.MAX_FILE_SIZE = 100 * 1024     -- 100KB per file limit
SavedVarsDriver.MAX_TOTAL_SIZE = 5 * 1024 * 1024 -- 5MB total limit
SavedVarsDriver.CURRENT_SCHEMA = 6             -- Schema version

---Get the current character's home directory path
---@return string path e.g. "/home/Argent Dawn/Ludwig"
---@return string realm e.g. "Argent Dawn"
---@return string name e.g. "Ludwig"
function SavedVarsDriver._getCharacterHome()
    local name, realm = UnitFullName("player")
    -- Realm can be nil for home realm, use GetRealmName() as fallback
    if not realm or realm == "" then
        realm = GetRealmName()
    end
    -- Fallback in case APIs aren't ready yet (shouldn't happen after PLAYER_LOGIN)
    name = name or "Unknown"
    realm = realm or "Unknown"
    return "/home/" .. realm .. "/" .. name, realm, name
end

-- Optional compression
local LibDeflate = LibStub and LibStub("LibDeflate", true)

---Create a new Sharded Driver
---@param dbRef table Reference to Commander3DB
function SavedVarsDriver:init(dbRef)
    self.db = dbRef

    -- Initialize or Migrate
    self:_ensureSchema()

    -- Shortcuts for speed
    self.meta = self.db.vfs.meta
    self.data = self.db.vfs.data
    self.stats = self.db.vfs.stats

    -- Ensure per-character folders exist on every login.
    self:_ensureDefaultDirs()

    -- Tag current character folder with class metadata (if available).
    self:_tagCharacterHome()

    -- Clean up any stray root child entries.
    self:_sanitizeRootChildren()
end

-------------------------------------------------------------------------------
-- Schema & Migration
-------------------------------------------------------------------------------

function SavedVarsDriver:_ensureSchema()
    -- Initialize root DB if missing
    if not self.db.vfs then
        self.db.vfs = { version = 0 }
    end

    -- Check version
    local ver = self.db.vfs.version or 0

    -- Full reset needed for very old schemas (v0-v4)
    if ver < 5 then
        C3:Debug(string.format("VFS: Migrating Schema v%d -> v%d (full reset)", ver, self.CURRENT_SCHEMA))

        -- Store old data for migration
        local oldNodes = self.db.vfs.nodes or {}

        -- Reset Structure
        self.db.vfs = {
            version = self.CURRENT_SCHEMA,
            meta = {},          -- Path -> { type, size, created... }
            data = {},          -- Path -> content
            stats = {
                totalSize = 0,
                fileCount = 0,
                dirCount = 0
            }
        }

        -- Set shortcuts immediately so helper methods work
        self.meta = self.db.vfs.meta
        self.data = self.db.vfs.data
        self.stats = self.db.vfs.stats

        -- Initialize Root
        self:_createMeta("/", {
            name = "",
            type = "dir",
            children = {},
            created = GetTime(),
            modified = GetTime(),
        })

        -- Migrate old data if it existed
        self:_migrateOldData(oldNodes)

        -- Ensure default directories exist
        self:_ensureDefaultDirs()
    elseif ver < self.CURRENT_SCHEMA then
        -- Incremental migration from v5 to v6
        C3:Debug(string.format("VFS: Migrating Schema v%d -> v%d (incremental)", ver, self.CURRENT_SCHEMA))

        -- Set shortcuts first so migration methods work
        self.meta = self.db.vfs.meta
        self.stats = self.db.vfs.stats

        -- v5 -> v6: Flatten shards into single data table
        self:_migrateV5ToV6()

        -- Update shortcut after migration
        self.data = self.db.vfs.data

        -- Ensure character-specific default directories exist
        self:_ensureDefaultDirs()

        -- Update version
        self.db.vfs.version = self.CURRENT_SCHEMA
    end
end

---Migrate v5 to v6: Flatten shards into single data table
function SavedVarsDriver:_migrateV5ToV6()
    local shards = self.db.vfs.shards or {}
    local merged = {}

    -- Merge all shards into single data table
    for _, shard in pairs(shards) do
        if type(shard) == "table" then
            for path, content in pairs(shard) do
                merged[path] = content
            end
        end
    end

    -- Strip shard field from all file metadata
    for path, meta in pairs(self.meta) do
        if meta.type == "file" then
            meta.shard = nil
        end
    end

    -- Replace storage structure
    self.db.vfs.data = merged
    self.db.vfs.shards = nil
    self.db.vfs.currentShard = nil

    C3:Debug("VFS v5->v6: Flattened shards into single data table")
end

---Decompress legacy content (old schema format)
---@param content string Raw content from old schema
---@return string Decompressed content
function SavedVarsDriver:_decompressLegacy(content)
    if not content then return "" end
    if content:sub(1, 7) ~= "\001CMPRS\002" then return content end
    if not LibDeflate then return "" end

    local encoded = content:sub(8)
    local decoded = LibDeflate:DecodeForPrint(encoded)
    if not decoded then return "" end

    return LibDeflate:DecompressDeflate(decoded) or ""
end

function SavedVarsDriver:_migrateOldData(oldNodes)
    local count = 0

    for path, node in pairs(oldNodes) do repeat
        -- Guard: Skip root (already created)
        if path == "/" then break end

        -- Guard: Skip invalid nodes
        if not node or not node.type then break end

        if node.type == "dir" then
            self:MakeDirP(path)
        elseif node.type == "file" then
            local content = self:_decompressLegacy(node.content)
            self:Write(path, content)
        end

        count = count + 1
    until true end

    if count > 0 then
        C3:Debug("VFS: Migrated " .. count .. " items successfully.")
    end
end

function SavedVarsDriver:_ensureDefaultDirs()
    -- Get character home path
    local charHome, realm, name = SavedVarsDriver._getCharacterHome()

    -- Ensure root exists and has children table
    if not self.meta["/"] then
        self:_createMeta("/", {
            name = "",
            type = "dir",
            children = {},
            created = GetTime(),
            modified = GetTime(),
        })
    elseif self.meta["/"].type == "dir" and not self.meta["/"].children then
        self.meta["/"].children = {}
    end

    local defaults = {
        -- System directories
        "/sys",
        "/sys/config",
        "/sys/cache",
        "/tmp",
        -- Character home structure: /home/<Realm>/<Character>/
        "/home",
        "/home/" .. realm,
        charHome,
        charHome .. "/scripts",
        charHome .. "/aliases",
        charHome .. "/notes",
    }
    for _, path in ipairs(defaults) do
        self:_ensureDir(path)
    end
end

---Ensure a directory exists and is linked from its parent
---@param path string
function SavedVarsDriver:_ensureDir(path)
    if not path or path == "" then return end

    if not self:Exists(path) then
        self:MakeDirP(path)
    end

    local node = self.meta[path]
    if not node or node.type ~= "dir" then return end
    node.children = node.children or {}

    if path == "/" then return end

    local parentPath = C3.Paths.Parent(path)
    if not self:Exists(parentPath) then
        self:MakeDirP(parentPath)
    end

    local parent = self.meta[parentPath]
    if parent and parent.type == "dir" then
        parent.children = parent.children or {}
        local name = C3.Paths.Filename(path)
        if name ~= "" and not parent.children[name] then
            parent.children[name] = true
            parent.modified = GetTime()
        end
    end
end

---Remove invalid root child entries (e.g., empty name)
function SavedVarsDriver:_sanitizeRootChildren()
    local root = self.meta["/"]
    if root and root.children and root.children[""] then
        root.children[""] = nil
        root.modified = GetTime()
    end
end

---Tag the current character home directory with class metadata (if available)
function SavedVarsDriver:_tagCharacterHome()
    local charHome = SavedVarsDriver._getCharacterHome()
    local _, classFilename = UnitClass("player")
    if not classFilename then return end

    local node = self.meta[charHome]
    if node and node.type == "dir" then
        node.metadata = node.metadata or {}
        node.metadata.class = classFilename
    end
end

---Public helper to refresh character metadata on login
function SavedVarsDriver:TagCharacterHome()
    self:_tagCharacterHome()
end

---Set metadata on a node
---@param path string
---@param data table
---@return boolean success
---@return string? error
function SavedVarsDriver:SetMetadata(path, data)
    local node = self.meta[path]
    if not node then
        return false, "Path not found: " .. path
    end

    node.metadata = node.metadata or {}
    for key, value in pairs(data or {}) do
        node.metadata[key] = value
    end
    node.modified = GetTime()
    return true
end

-------------------------------------------------------------------------------
-- Internal Helpers
-------------------------------------------------------------------------------

---Create/Update metadata entry
function SavedVarsDriver:_createMeta(path, data)
    self.db.vfs.meta[path] = data
end

---Compress content
function SavedVarsDriver:_compress(content)
    if not LibDeflate or #content < 256 then
        return content, false
    end
    local compressed = LibDeflate:CompressDeflate(content)
    local encoded = LibDeflate:EncodeForPrint(compressed)

    if #encoded < #content then
        return "\001" .. encoded, true -- \001 is our marker
    end
    return content, false
end

---Decompress content
function SavedVarsDriver:_decompress(stored)
    if not stored then return "" end
    if stored:sub(1, 1) == "\001" then
        if not LibDeflate then return "" end
        local decoded = LibDeflate:DecodeForPrint(stored:sub(2))
        if not decoded then return "" end
        return LibDeflate:DecompressDeflate(decoded) or ""
    end
    return stored
end

-------------------------------------------------------------------------------
-- Public API: Path Operations
-------------------------------------------------------------------------------

function SavedVarsDriver:Exists(path)
    return self.meta[path] ~= nil
end

function SavedVarsDriver:IsDirectory(path)
    local node = self.meta[path]
    return node and node.type == "dir"
end

function SavedVarsDriver:IsFile(path)
    local node = self.meta[path]
    return node and node.type == "file"
end

function SavedVarsDriver:GetNode(path)
    return self.meta[path]
end

-------------------------------------------------------------------------------
-- Public API: File I/O
-------------------------------------------------------------------------------

function SavedVarsDriver:Read(path)
    local meta = self.meta[path]
    if not meta then return nil, "File not found: " .. path end
    if meta.type == "dir" then return nil, "Is a directory: " .. path end

    return self:_decompress(self.data[path])
end

function SavedVarsDriver:Write(path, content)
    local parentPath = C3.Paths.Parent(path)
    local fileName = C3.Paths.Filename(path)

    -- Validate Parent
    local parent = self.meta[parentPath]
    if not parent then return false, "Parent directory missing: " .. parentPath end
    if parent.type ~= "dir" then return false, "Parent is not directory: " .. parentPath end

    -- Check Sizes
    local contentSize = #(content or "")
    if contentSize > self.MAX_FILE_SIZE then
        return false, string.format("File too large (Max: %dKB)", self.MAX_FILE_SIZE / 1024)
    end
    if self.stats.totalSize + contentSize > self.MAX_TOTAL_SIZE then
        return false, "Disk quota exceeded"
    end

    -- Check if file exists (to update existing)
    local meta = self.meta[path]
    local isNew = not meta

    if meta then
        -- Update stats (subtract old size)
        self.stats.totalSize = self.stats.totalSize - (meta.size or 0)
    end

    -- Process Data
    local stored, compressed = self:_compress(content or "")

    -- Write Data
    self.data[path] = stored

    -- Write Metadata
    local now = GetTime()
    self.meta[path] = {
        name = fileName,
        type = "file",
        size = contentSize,
        compressed = compressed,
        created = meta and meta.created or now,
        modified = now
    }

    -- Update Parent
    parent.children[fileName] = true
    parent.modified = now

    -- Update Stats
    self.stats.totalSize = self.stats.totalSize + contentSize
    if isNew then self.stats.fileCount = self.stats.fileCount + 1 end

    -- Warn user when approaching quota limit (80% threshold)
    local WARN_THRESHOLD = self.MAX_TOTAL_SIZE * 0.8
    if self.stats.totalSize > WARN_THRESHOLD and not self._quotaWarned then
        self._quotaWarned = true
        local pct = math.floor((self.stats.totalSize / self.MAX_TOTAL_SIZE) * 100)
        C3:Print("|cFFFF8800Warning: VFS storage at " .. pct .. "% capacity (" ..
            string.format("%.1f", self.stats.totalSize / 1024 / 1024) .. "MB / " ..
            string.format("%.0f", self.MAX_TOTAL_SIZE / 1024 / 1024) .. "MB)|r")
    end

    return true
end

function SavedVarsDriver:Append(path, content)
    local current, _ = self:Read(path)
    return self:Write(path, (current or "") .. (content or ""))
end

function SavedVarsDriver:Delete(path)
    if path == "/" then
        return false, "Cannot delete root"
    end

    local meta = self.meta[path]
    if not meta then return false, "Not found: " .. path end

    if meta.type == "dir" then
        if next(meta.children) then return false, "Directory not empty: " .. path end
    end

    -- Remove Data if file
    if meta.type == "file" then
        self.data[path] = nil
        self.stats.totalSize = self.stats.totalSize - (meta.size or 0)
        self.stats.fileCount = self.stats.fileCount - 1
    else
        self.stats.dirCount = self.stats.dirCount - 1
    end

    -- Remove from Parent
    local parent = self.meta[C3.Paths.Parent(path)]
    if parent then
        parent.children[meta.name] = nil
        parent.modified = GetTime()
    end

    -- Remove Metadata
    self.meta[path] = nil
    return true
end

-------------------------------------------------------------------------------
-- Public API: Directory Operations
-------------------------------------------------------------------------------

function SavedVarsDriver:MakeDir(path)
    if self:Exists(path) then return false, "Exists: " .. path end

    local parentPath = C3.Paths.Parent(path)
    local parent = self.meta[parentPath]

    if not parent or parent.type ~= "dir" then
        return false, "Invalid parent: " .. parentPath
    end

    local name = C3.Paths.Filename(path)
    local now = GetTime()

    -- Write Meta
    self.meta[path] = {
        name = name,
        type = "dir",
        children = {},
        created = now,
        modified = now
    }

    -- Link Parent
    parent.children[name] = true
    parent.modified = now

    self.stats.dirCount = self.stats.dirCount + 1
    return true
end

function SavedVarsDriver:MakeDirP(path)
    -- Guard: Already exists as directory
    if self:IsDirectory(path) then return true end

    -- Guard: Exists but is not a directory
    if self:Exists(path) then
        return false, "Path exists but is not a directory: " .. path
    end

    -- Guard: Ensure parent exists (recursive)
    local parent = C3.Paths.Parent(path)
    if parent ~= "/" and not self:Exists(parent) then
        local ok, err = self:MakeDirP(parent)
        if not ok then return false, err end
    end

    return self:MakeDir(path)
end

function SavedVarsDriver:List(path)
    local meta = self.meta[path]
    if not meta or meta.type ~= "dir" then return nil, "Not a directory: " .. path end

    local entries = {}
    for name, _ in pairs(meta.children or {}) do
        local childPath = C3.Paths.Join(path, name)
        local childMeta = self.meta[childPath]
        if childMeta then
            table.insert(entries, {
                name = name,
                path = childPath,
                type = childMeta.type,
                size = childMeta.size or 0,
                modified = childMeta.modified
            })
        end
    end

    -- Sort: Dirs first, then alphabetical
    table.sort(entries, function(a, b)
        if a.type ~= b.type then return a.type == "dir" end
        return a.name < b.name
    end)

    return entries
end

function SavedVarsDriver:DeleteRecursive(path)
    if path == "/" then
        return false, "Cannot delete root"
    end

    local meta = self.meta[path]
    if not meta then return false, "Not found: " .. path end

    if meta.type == "dir" then
        for name, _ in pairs(meta.children or {}) do
            local childPath = C3.Paths.Join(path, name)
            self:DeleteRecursive(childPath)
        end
    end
    return self:Delete(path)
end

-------------------------------------------------------------------------------
-- Public API: Move/Copy
-------------------------------------------------------------------------------

function SavedVarsDriver:Move(oldPath, newPath)
    local meta = self.meta[oldPath]

    -- Guard: Source must exist
    if not meta then
        return false, "Source not found: " .. oldPath
    end

    -- Guard: Destination must not exist
    if self:Exists(newPath) then
        return false, "Destination already exists: " .. newPath
    end

    -- Guard: Destination parent must be a directory
    local newParent = C3.Paths.Parent(newPath)
    if not self:IsDirectory(newParent) then
        return false, "Destination parent is not a directory: " .. newParent
    end

    -- Handle file move (simple case)
    if meta.type == "file" then
        local content, err = self:Read(oldPath)
        if not content then return false, err end

        self:Delete(oldPath)
        return self:Write(newPath, content)
    end

    -- Handle directory move (recursive)
    local ok, err = self:MakeDir(newPath)
    if not ok then return false, err end

    for childName in pairs(meta.children or {}) do
        local oldChildPath = C3.Paths.Join(oldPath, childName)
        local newChildPath = C3.Paths.Join(newPath, childName)
        self:Move(oldChildPath, newChildPath)
    end

    self:Delete(oldPath)
    return true
end

function SavedVarsDriver:Copy(srcPath, destPath)
    local content, err = self:Read(srcPath)
    if not content then
        return false, err
    end

    return self:Write(destPath, content)
end

-------------------------------------------------------------------------------
-- Stats
-------------------------------------------------------------------------------

function SavedVarsDriver:GetStats()
    return {
        files = self.stats.fileCount,
        directories = self.stats.dirCount,
        totalSize = self.stats.totalSize,
    }
end

function SavedVarsDriver:GetTotalSize()
    return self.stats.totalSize
end

function SavedVarsDriver:GetNodeCount()
    return self.stats.fileCount + self.stats.dirCount
end

-------------------------------------------------------------------------------
-- Repair & Validation
-------------------------------------------------------------------------------

---Validate and repair VFS metadata/shard consistency
---@return table report { issues = {}, fixes = {}, stats = { before = {}, after = {} } }
function SavedVarsDriver:Repair()
    local report = {
        issues = {},
        fixes = {},
        stats = {
            before = {
                totalSize = self.stats.totalSize,
                fileCount = self.stats.fileCount,
                dirCount = self.stats.dirCount,
            },
            after = {},
        },
    }

    -- Track actual counts for recalculation
    local actualTotalSize = 0
    local actualFileCount = 0
    local actualDirCount = 0

    -- Phase 1: Validate all metadata entries
    for path, meta in pairs(self.meta) do repeat
        -- Guard: Root is special case
        if path == "/" then
            if meta.type == "dir" then
                actualDirCount = actualDirCount + 1
            end
            break
        end

        -- Check 1: Parent must exist and be a directory
        local parentPath = C3.Paths.Parent(path)
        local parent = self.meta[parentPath]
        if not parent then
            table.insert(report.issues, {
                type = "orphan_no_parent",
                path = path,
                detail = "Parent directory missing: " .. parentPath,
            })
            -- Fix: Create parent chain
            local ok = self:MakeDirP(parentPath)
            if ok then
                parent = self.meta[parentPath]
                table.insert(report.fixes, {
                    type = "created_parent",
                    path = parentPath,
                })
            end
        elseif parent.type ~= "dir" then
            table.insert(report.issues, {
                type = "parent_not_dir",
                path = path,
                detail = "Parent is not a directory: " .. parentPath,
            })
            -- Cannot auto-fix: would need to delete parent file
        end

        -- Check 2: Parent-child link must exist
        if parent and parent.children then
            local name = meta.name or C3.Paths.Filename(path)
            if not parent.children[name] then
                table.insert(report.issues, {
                    type = "missing_child_link",
                    path = path,
                    detail = "Not linked in parent's children table",
                })
                -- Fix: Add child link
                parent.children[name] = true
                table.insert(report.fixes, {
                    type = "added_child_link",
                    path = path,
                })
            end
        end

        -- Check 3: For files, data must exist
        if meta.type == "file" then
            actualFileCount = actualFileCount + 1

            if self.data[path] == nil then
                table.insert(report.issues, {
                    type = "missing_file_data",
                    path = path,
                    detail = "File data missing",
                })
                -- Fix: Write empty content
                self.data[path] = ""
                meta.size = 0
                table.insert(report.fixes, {
                    type = "reset_file_content",
                    path = path,
                })
            end

            actualTotalSize = actualTotalSize + (meta.size or 0)
        elseif meta.type == "dir" then
            actualDirCount = actualDirCount + 1

            -- Check 4: Directory must have children table
            if not meta.children then
                table.insert(report.issues, {
                    type = "missing_children_table",
                    path = path,
                })
                meta.children = {}
                table.insert(report.fixes, {
                    type = "added_children_table",
                    path = path,
                })
            end
        end
    until true end

    -- Phase 2: Check for orphaned data (data with no metadata)
    for path, _ in pairs(self.data) do
        if not self.meta[path] then
            table.insert(report.issues, {
                type = "orphan_data",
                path = path,
                detail = "Data exists but no metadata",
            })
            -- Fix: Remove orphaned data
            self.data[path] = nil
            table.insert(report.fixes, {
                type = "removed_orphan_data",
                path = path,
            })
        end
    end

    -- Phase 3: Check for stale child links (child listed but doesn't exist)
    for path, meta in pairs(self.meta) do repeat
        if meta.type ~= "dir" then break end
        if not meta.children then break end

        for childName, _ in pairs(meta.children) do
            local childPath = C3.Paths.Join(path, childName)
            if not self.meta[childPath] then
                table.insert(report.issues, {
                    type = "stale_child_link",
                    path = path,
                    childName = childName,
                    detail = "Child listed but does not exist: " .. childPath,
                })
                -- Fix: Remove stale link
                meta.children[childName] = nil
                table.insert(report.fixes, {
                    type = "removed_stale_link",
                    path = path,
                    childName = childName,
                })
            end
        end
    until true end

    -- Phase 4: Reconcile stats
    if self.stats.totalSize ~= actualTotalSize then
        table.insert(report.issues, {
            type = "stats_mismatch",
            field = "totalSize",
            expected = actualTotalSize,
            actual = self.stats.totalSize,
        })
        self.stats.totalSize = actualTotalSize
        table.insert(report.fixes, {
            type = "corrected_stats",
            field = "totalSize",
            value = actualTotalSize,
        })
    end

    if self.stats.fileCount ~= actualFileCount then
        table.insert(report.issues, {
            type = "stats_mismatch",
            field = "fileCount",
            expected = actualFileCount,
            actual = self.stats.fileCount,
        })
        self.stats.fileCount = actualFileCount
        table.insert(report.fixes, {
            type = "corrected_stats",
            field = "fileCount",
            value = actualFileCount,
        })
    end

    if self.stats.dirCount ~= actualDirCount then
        table.insert(report.issues, {
            type = "stats_mismatch",
            field = "dirCount",
            expected = actualDirCount,
            actual = self.stats.dirCount,
        })
        self.stats.dirCount = actualDirCount
        table.insert(report.fixes, {
            type = "corrected_stats",
            field = "dirCount",
            value = actualDirCount,
        })
    end

    -- Record final stats
    report.stats.after = {
        totalSize = self.stats.totalSize,
        fileCount = self.stats.fileCount,
        dirCount = self.stats.dirCount,
    }

    -- Clear quota warning if we freed space
    if self.stats.totalSize < self.MAX_TOTAL_SIZE * 0.8 then
        self._quotaWarned = false
    end

    return report
end

return SavedVarsDriver
