--[[
    Commander 3.0 - Object Pipeline

    The core of the "PowerShell" paradigm.
    Data flows between commands as Lua tables (objects), not strings.

    Patterns:
    - Producer: Creates objects (bags, mounts, spells)
    - Transformer: Filters/modifies objects (where, sort, take)
    - Consumer: Final destination (use, display, export)
]]

local ADDON_NAME, C3 = ...

local Pipeline = {}
Pipeline.__index = Pipeline
C3.Pipeline = Pipeline

-------------------------------------------------------------------------------
-- Iterator Helpers
-------------------------------------------------------------------------------

---Create an iterator from an array
---@param array table
---@return function iterator
local function arrayIterator(array)
    local i = 0
    local n = #array
    return function()
        i = i + 1
        if i <= n then
            return array[i]
        end
    end
end

---Check if we're in a yieldable coroutine context
---@return boolean
local function canYield()
    local co, isMain = coroutine.running()
    -- Can yield if we're in a coroutine (not main thread)
    return co ~= nil and not isMain
end

---Convert an iterator to an array
---Note: We no longer yield here because iterating over coroutine.wrap'd
---iterators creates a C-call boundary that prevents yielding.
---@param iterator function
---@return table array
local function iteratorToArray(iterator)
    local result = {}
    for item in iterator do
        table.insert(result, item)
    end
    return result
end

---Check if a value is an iterator (function)
---@param value any
---@return boolean
local function isIterator(value)
    return type(value) == "function"
end

---Normalize a value to an iterator
---@param value any
---@return function iterator
local function toIterator(value)
    if isIterator(value) then
        return value
    elseif type(value) == "table" then
        -- Check if it's a typed object (single item) vs an array
        -- Typed objects have _type field and should be treated as single values
        if value._type then
            local returned = false
            return function()
                if not returned then
                    returned = true
                    return value
                end
            end
        else
            return arrayIterator(value)
        end
    else
        -- Single value (string, number, etc.)
        local returned = false
        return function()
            if not returned then
                returned = true
                return value
            end
        end
    end
end

-------------------------------------------------------------------------------
-- Pipeline Stage
-------------------------------------------------------------------------------

local Stage = C3.class()

function Stage:init(fn, name)
    self.fn = fn
    self.name = name or "anonymous"
end

---Execute the stage with input
---@param input any
---@param context table
---@return any output
function Stage:Execute(input, context)
    return self.fn(input, context)
end

-------------------------------------------------------------------------------
-- Pipeline Builder
-------------------------------------------------------------------------------

---Create a new pipeline
---@return table
function Pipeline.new()
    local self = setmetatable({}, Pipeline)
    self.stages = {}
    return self
end

---Add a stage to the pipeline
---@param fn function
---@param name? string
---@return Pipeline self For chaining
function Pipeline:Add(fn, name)
    table.insert(self.stages, Stage:new(fn, name))
    return self
end

---Execute the pipeline
---@param context table
---@return any result
function Pipeline:Run(context)
    local result = nil

    for i, stage in ipairs(self.stages) do
        result = stage:Execute(result, context)

        -- Allow yielding between stages (for async)
        if i % 3 == 0 then
            coroutine.yield()
        end
    end

    return result
end

-------------------------------------------------------------------------------
-- Pipeline Execution
-------------------------------------------------------------------------------

---Execute an AST pipeline
---@param ast table Parsed AST
---@param context table Execution context
---@return any result
function Pipeline:Execute(ast, context)
    context = context or {}

    if not ast then
        return C3.Types.Error("Empty AST")
    end

    -- Handle different AST node types
    if ast.type == "Pipeline" then
        return self:ExecutePipeline(ast, context)
    elseif ast.type == "Chain" then
        return self:ExecuteChain(ast, context)
    elseif ast.type == "Command" then
        return self:ExecuteCommand(ast, context)
    else
        return C3.Types.Error("Unknown AST type: " .. tostring(ast.type))
    end
end

---Execute synchronously (no async/yielding)
---@param ast table
---@param context table
---@return any
function Pipeline:ExecuteSync(ast, context)
    context = context or {}
    context._sync = true

    if not ast then
        return C3.Types.Error("Empty AST")
    end

    if ast.type == "Pipeline" then
        return self:ExecutePipelineSync(ast, context)
    elseif ast.type == "Chain" then
        return self:ExecuteChainSync(ast, context)
    elseif ast.type == "Command" then
        return self:ExecuteCommandSync(ast, context)
    else
        return C3.Types.Error("Unknown AST type: " .. tostring(ast.type))
    end
end

---Execute a pipeline (cmd | cmd | cmd)
---@param ast table
---@param context table
---@return any
function Pipeline:ExecutePipeline(ast, context)
    local result = nil

    for i, stage in ipairs(ast.stages) do
        -- Pass previous result as input
        context._input = result

        -- Execute stage
        result = self:ExecuteCommand(stage, context)

        -- DON'T materialize between stages - let iterators chain lazily!
        -- This allows take/first to short-circuit large datasets.
        -- Commands that need materialization (sort, last, group) do it themselves.

        -- Yield for async
        coroutine.yield()
    end

    -- Final result: materialize if iterator
    if isIterator(result) then
        result = iteratorToArray(result)
    end

    return result
end

---Execute pipeline synchronously
---@param ast table
---@param context table
---@return any
function Pipeline:ExecutePipelineSync(ast, context)
    local result = nil

    for i, stage in ipairs(ast.stages) do
        context._input = result
        result = self:ExecuteCommandSync(stage, context)

        -- DON'T materialize between stages - let iterators chain lazily!
    end

    if isIterator(result) then
        result = iteratorToArray(result)
    end

    return result
end

---Execute a chain (cmd && cmd ; cmd)
---@param ast table
---@param context table
---@return any
function Pipeline:ExecuteChain(ast, context)
    local lastResult = nil
    local lastSuccess = true
    local prevOperator = nil

    for _, segment in ipairs(ast.segments) do
        local shouldExecute = true

        -- Check operator from the previous segment to gate this one
        if prevOperator == "&&" and not lastSuccess then
            shouldExecute = false
        end
        -- ";" always executes

        if shouldExecute then
            local result = self:ExecutePipeline(segment.pipeline, context)

            -- Determine success
            if C3.Types:IsType(result, "Error") then
                lastSuccess = false
            else
                lastSuccess = true
            end

            lastResult = result
        end

        prevOperator = segment.operator
        coroutine.yield()
    end

    return lastResult
end

---Execute chain synchronously
---@param ast table
---@param context table
---@return any
function Pipeline:ExecuteChainSync(ast, context)
    local lastResult = nil
    local lastSuccess = true
    local prevOperator = nil

    for _, segment in ipairs(ast.segments) do
        local shouldExecute = true

        if prevOperator == "&&" and not lastSuccess then
            shouldExecute = false
        end

        if shouldExecute then
            local result = self:ExecutePipelineSync(segment.pipeline, context)

            if C3.Types:IsType(result, "Error") then
                lastSuccess = false
            else
                lastSuccess = true
            end

            lastResult = result
        end

        prevOperator = segment.operator
    end

    return lastResult
end

---Execute a single command
---@param ast table
---@param context table
---@return any
function Pipeline:ExecuteCommand(ast, context)
    local cmdName = ast.name
    local args = ast.args or {}

    -- Look up command
    local cmd = C3.Commands[cmdName]
    if not cmd then
        -- Check aliases
        local aliasTarget = C3.Aliases[cmdName]
        if aliasTarget then
            cmd = C3.Commands[aliasTarget]
        end
    end

    if not cmd then
        return C3.Types.Error("Unknown command: " .. cmdName)
    end

    -- Build execution context using pool to reduce GC pressure
    local execContext = C3.Context.Acquire({
        _input = context._input,
        input = context.input,
        args = args,
        flags = ast.flags or {},
        session = context.session,
    })

    -- Execute command
    -- NOTE: We cannot use pcall() here because Lua 5.1 doesn't support
    -- yielding across C-call boundaries. Errors will propagate to Task:Poll()
    -- which handles them via coroutine.resume()'s error return.
    local result
    if type(cmd) == "function" then
        result = cmd(execContext)
    elseif type(cmd) == "table" and cmd.execute then
        -- Use DOT not COLON - execute(ctx) expects context as first arg, not self
        result = cmd.execute(execContext)
    else
        result = C3.Types.Error("Invalid command definition")
    end

    -- Release context back to pool
    C3.Context.Release(execContext)

    return result
end

---Execute command synchronously
---@param ast table
---@param context table
---@return any
function Pipeline:ExecuteCommandSync(ast, context)
    -- Same as async version but without yielding
    return self:ExecuteCommand(ast, context)
end

-------------------------------------------------------------------------------
-- Producer/Transform/Consumer Helpers
-------------------------------------------------------------------------------

---Create a producer (yields objects)
---Uses coroutine.wrap to create a lazy iterator.
---
---IMPORTANT: Command code inside generatorFn should NOT call coroutine.yield()
---directly - this breaks the iterator because it yields from the wrapped
---coroutine, not the Task coroutine. Throttling is handled automatically
---by iteratorToArray() when the pipeline materializes results.
---
---@param generatorFn function(emit) - Call emit(obj) for each object to produce
---@return function iterator
function Pipeline.Producer(generatorFn)
    return coroutine.wrap(function()
        generatorFn(function(obj)
            coroutine.yield(obj)
        end)
    end)
end

---Create a transformer (filters/maps objects)
---@param input any
---@param transformFn function(obj) -> obj|nil
---@return function iterator
function Pipeline.Transform(input, transformFn)
    local iter = toIterator(input)
    return coroutine.wrap(function()
        for obj in iter do
            local result = transformFn(obj)
            if result ~= nil then
                coroutine.yield(result)
            end
        end
    end)
end

---Consume an iterator (for side effects)
---@param input any
---@param consumeFn function(obj)
function Pipeline.Consume(input, consumeFn)
    local iter = toIterator(input)
    for obj in iter do
        consumeFn(obj)
    end
end

---Collect iterator to array
---@param input any
---@return table array
function Pipeline.Collect(input)
    if type(input) == "table" and not isIterator(input) then
        return input
    end
    return iteratorToArray(toIterator(input))
end

---Map over input
---@param input any
---@param mapFn function(obj) -> any
---@return table array
function Pipeline.Map(input, mapFn)
    local result = {}
    local iter = toIterator(input)
    for obj in iter do
        table.insert(result, mapFn(obj))
    end
    return result
end

---Filter input
---@param input any
---@param filterFn function(obj) -> boolean
---@return table array
function Pipeline.Filter(input, filterFn)
    local result = {}
    local iter = toIterator(input)
    for obj in iter do
        if filterFn(obj) then
            table.insert(result, obj)
        end
    end
    return result
end

return Pipeline
