"""
LUA Scripts for Atomic Inventory Operations.

These scripts execute atomically in Redis, ensuring no race conditions
between checking availability and reserving inventory.

Key Schema:
    inv:{warehouse_id}:{sku} -> HASH {
        "available": int,           # ERP quantity minus all reservations
        "erp_quantity": int,        # Raw quantity from ERP
        "reserved_medusa": int,     # Reserved by Medusa orders
        "reserved_shopify": int     # Reserved by Shopify orders
    }
"""

# RESERVE_SCRIPT
# Atomically checks availability and reserves inventory.
# Returns JSON with success status and current state.
#
# KEYS[1] = inv:{warehouse_id}:{sku}
# ARGV[1] = quantity to reserve (int)
# ARGV[2] = source ("medusa" or "shopify")
RESERVE_SCRIPT = """
local key = KEYS[1]
local qty = tonumber(ARGV[1])
local source = ARGV[2]

-- Validate inputs
if qty <= 0 then
    return cjson.encode({
        success = false,
        reason = "invalid_quantity",
        message = "Quantity must be positive"
    })
end

-- Get current state
local available = tonumber(redis.call('HGET', key, 'available') or '0')
local reserved_key = 'reserved_' .. source
local current_reserved = tonumber(redis.call('HGET', key, reserved_key) or '0')

-- Check if enough available
if available < qty then
    return cjson.encode({
        success = false,
        reason = "insufficient_stock",
        available = available,
        requested = qty,
        reserved = current_reserved,
        source = source
    })
end

-- Atomic reserve: decrement available, increment reserved
redis.call('HINCRBY', key, 'available', -qty)
redis.call('HINCRBY', key, reserved_key, qty)

-- Get updated values
local new_available = tonumber(redis.call('HGET', key, 'available'))
local new_reserved = tonumber(redis.call('HGET', key, reserved_key))

return cjson.encode({
    success = true,
    available = new_available,
    reserved = new_reserved,
    source = source,
    quantity_reserved = qty
})
"""

# RELEASE_SCRIPT
# Atomically releases reserved inventory.
# For cancellation: returns items to available stock.
# For fulfillment: items are shipped out, available stays unchanged, erp_quantity decreases.
#
# KEYS[1] = inv:{warehouse_id}:{sku}
# ARGV[1] = quantity to release (int)
# ARGV[2] = source ("medusa" or "shopify")
# ARGV[3] = reason ("fulfilled", "cancelled", "expired")
RELEASE_SCRIPT = """
local key = KEYS[1]
local qty = tonumber(ARGV[1])
local source = ARGV[2]
local reason = ARGV[3] or 'cancelled'

-- Validate inputs
if qty <= 0 then
    return cjson.encode({
        success = false,
        reason = "invalid_quantity",
        message = "Quantity must be positive"
    })
end

-- Get current reserved for this source
local reserved_key = 'reserved_' .. source
local current_reserved = tonumber(redis.call('HGET', key, reserved_key) or '0')

-- Can't release more than reserved
local release_qty = math.min(qty, current_reserved)

if release_qty == 0 then
    -- Nothing to release
    local available = tonumber(redis.call('HGET', key, 'available') or '0')
    local erp_quantity = tonumber(redis.call('HGET', key, 'erp_quantity') or '0')
    return cjson.encode({
        success = true,
        released = 0,
        available = available,
        reserved = current_reserved,
        erp_quantity = erp_quantity,
        source = source,
        reason = reason,
        message = "Nothing to release"
    })
end

-- Always decrement reserved count
redis.call('HINCRBY', key, reserved_key, -release_qty)

-- Also decrement confirmed count (can't release more confirmed than exists)
local confirmed_key = 'confirmed_' .. source
local current_confirmed = tonumber(redis.call('HGET', key, confirmed_key) or '0')
local confirmed_to_release = math.min(release_qty, current_confirmed)
if confirmed_to_release > 0 then
    redis.call('HINCRBY', key, confirmed_key, -confirmed_to_release)
end

-- Handle based on reason
-- 'fulfilled' means item is shipped out
-- 'cancelled' or 'expired' means item returns to available stock
if reason ~= 'fulfilled' then
    -- Cancellation/Expiry: item returns to available stock
    redis.call('HINCRBY', key, 'available', release_qty)
end
-- For 'fulfilled': do nothing with available/erp_quantity
-- ERP will send updated quantity and available will be recalculated

-- Get updated values
local new_available = tonumber(redis.call('HGET', key, 'available'))
local new_reserved = tonumber(redis.call('HGET', key, reserved_key))
local new_erp_quantity = tonumber(redis.call('HGET', key, 'erp_quantity') or '0')

return cjson.encode({
    success = true,
    released = release_qty,
    available = new_available,
    reserved = new_reserved,
    erp_quantity = new_erp_quantity,
    source = source,
    reason = reason
})
"""

# CONFIRM_RESERVATION_SCRIPT
# Called when ERP confirms/accepts an order (handoff).
# Moves reservation from 'unconfirmed' to 'confirmed' state.
# This prevents double-deduction: confirmed reservations are not subtracted from ERP qty.
#
# KEYS[1] = inv:{warehouse_id}:{sku}
# ARGV[1] = quantity to confirm (int)
# ARGV[2] = source ("medusa" or "shopify")
CONFIRM_RESERVATION_SCRIPT = """
local key = KEYS[1]
local qty = tonumber(ARGV[1])
local source = ARGV[2]

-- Validate inputs
if qty <= 0 then
    return cjson.encode({
        success = false,
        reason = "invalid_quantity",
        message = "Quantity must be positive"
    })
end

-- Get current reserved and confirmed for this source
local reserved_key = 'reserved_' .. source
local confirmed_key = 'confirmed_' .. source
local current_reserved = tonumber(redis.call('HGET', key, reserved_key) or '0')
local current_confirmed = tonumber(redis.call('HGET', key, confirmed_key) or '0')

-- Calculate how much we can confirm (can't confirm more than unconfirmed)
local unconfirmed = current_reserved - current_confirmed
local confirm_qty = math.min(qty, unconfirmed)

if confirm_qty <= 0 then
    -- Nothing to confirm
    local available = tonumber(redis.call('HGET', key, 'available') or '0')
    return cjson.encode({
        success = true,
        confirmed = 0,
        available = available,
        reserved = current_reserved,
        confirmed_total = current_confirmed,
        source = source,
        message = "Nothing to confirm"
    })
end

-- Increment confirmed count (reservation moves from unconfirmed to confirmed)
redis.call('HINCRBY', key, confirmed_key, confirm_qty)

-- Recalculate available: with fewer unconfirmed, available increases
-- available = erp_qty - (reserved - confirmed) = erp_qty - unconfirmed
local erp_qty = tonumber(redis.call('HGET', key, 'erp_quantity') or '0')
local new_confirmed = current_confirmed + confirm_qty
local new_unconfirmed = current_reserved - new_confirmed
local new_available = erp_qty - new_unconfirmed

if new_available < 0 then
    new_available = 0
end

redis.call('HSET', key, 'available', new_available)

return cjson.encode({
    success = true,
    confirmed = confirm_qty,
    available = new_available,
    reserved = current_reserved,
    confirmed_total = new_confirmed,
    unconfirmed = new_unconfirmed,
    source = source
})
"""

# SET_INVENTORY_SCRIPT
# Called by Storehouse when ERP sends inventory updates.
# ERP sends SELLABLE/AVAILABLE quantity directly - this IS what's available to sell.
# We store it directly as 'available' without subtracting reservations.
# Reservations are temporary holds during order processing and are released when ERP confirms.
#
# KEYS[1] = inv:{warehouse_id}:{sku}
# ARGV[1] = erp_quantity (int) - the sellable quantity from ERP
SET_INVENTORY_SCRIPT = """
local key = KEYS[1]
local erp_qty = tonumber(ARGV[1])

-- Validate input
if erp_qty < 0 then
    return cjson.encode({
        success = false,
        reason = "invalid_quantity",
        message = "ERP quantity cannot be negative"
    })
end

-- Get current reservations and confirmed counts
local reserved_medusa = tonumber(redis.call('HGET', key, 'reserved_medusa') or '0')
local reserved_shopify = tonumber(redis.call('HGET', key, 'reserved_shopify') or '0')
local confirmed_medusa = tonumber(redis.call('HGET', key, 'confirmed_medusa') or '0')
local confirmed_shopify = tonumber(redis.call('HGET', key, 'confirmed_shopify') or '0')

-- Calculate unconfirmed reservations (orders ERP hasn't processed yet)
-- These need to be subtracted from ERP quantity
local unconfirmed_medusa = reserved_medusa - confirmed_medusa
local unconfirmed_shopify = reserved_shopify - confirmed_shopify
local total_unconfirmed = unconfirmed_medusa + unconfirmed_shopify

-- Ensure unconfirmed doesn't go negative (defensive)
if total_unconfirmed < 0 then
    total_unconfirmed = 0
end

-- Calculate available: ERP quantity minus UNCONFIRMED reservations only
-- Confirmed reservations are already accounted for in ERP's quantity
local new_available = erp_qty - total_unconfirmed

-- Ensure available doesn't go negative
if new_available < 0 then
    new_available = 0
end

-- Update hash fields
-- erp_quantity: the raw value from ERP (audit trail)
-- available: what we can sell (same as erp_qty since ERP sends sellable)
redis.call('HSET', key, 'erp_quantity', erp_qty)
redis.call('HSET', key, 'available', new_available)

return cjson.encode({
    success = true,
    erp_quantity = erp_qty,
    available = new_available,
    reserved_medusa = reserved_medusa,
    reserved_shopify = reserved_shopify,
    confirmed_medusa = confirmed_medusa,
    confirmed_shopify = confirmed_shopify,
    unconfirmed_medusa = unconfirmed_medusa,
    unconfirmed_shopify = unconfirmed_shopify,
    total_unconfirmed = total_unconfirmed
})
"""

# CHECK_AVAILABLE_SCRIPT
# Simple read of current available quantity.
# Can be used for pre-checkout availability checks.
#
# KEYS[1] = inv:{warehouse_id}:{sku}
CHECK_AVAILABLE_SCRIPT = """
local key = KEYS[1]
local available = tonumber(redis.call('HGET', key, 'available') or '0')
return available
"""

# GET_INVENTORY_STATE_SCRIPT
# Returns full inventory state for a SKU/warehouse.
# Useful for debugging and monitoring.
#
# KEYS[1] = inv:{warehouse_id}:{sku}
GET_INVENTORY_STATE_SCRIPT = """
local key = KEYS[1]

local available = tonumber(redis.call('HGET', key, 'available') or '0')
local erp_quantity = tonumber(redis.call('HGET', key, 'erp_quantity') or '0')
local reserved_medusa = tonumber(redis.call('HGET', key, 'reserved_medusa') or '0')
local reserved_shopify = tonumber(redis.call('HGET', key, 'reserved_shopify') or '0')
local confirmed_medusa = tonumber(redis.call('HGET', key, 'confirmed_medusa') or '0')
local confirmed_shopify = tonumber(redis.call('HGET', key, 'confirmed_shopify') or '0')

return cjson.encode({
    available = available,
    erp_quantity = erp_quantity,
    reserved_medusa = reserved_medusa,
    reserved_shopify = reserved_shopify,
    confirmed_medusa = confirmed_medusa,
    confirmed_shopify = confirmed_shopify,
    total_reserved = reserved_medusa + reserved_shopify,
    total_confirmed = confirmed_medusa + confirmed_shopify,
    unconfirmed_medusa = reserved_medusa - confirmed_medusa,
    unconfirmed_shopify = reserved_shopify - confirmed_shopify
})
"""

# BULK_SET_INVENTORY_SCRIPT
# Sets inventory for multiple SKUs in a single atomic operation.
# Used during bulk ERP updates.
#
# KEYS = list of inv:{warehouse_id}:{sku} keys
# ARGV = list of erp quantities (same order as KEYS)
BULK_SET_INVENTORY_SCRIPT = """
local results = {}

for i, key in ipairs(KEYS) do
    local erp_qty = tonumber(ARGV[i])
    
    if erp_qty and erp_qty >= 0 then
        -- Get current reservations
        local reserved_medusa = tonumber(redis.call('HGET', key, 'reserved_medusa') or '0')
        local reserved_shopify = tonumber(redis.call('HGET', key, 'reserved_shopify') or '0')
        local total_reserved = reserved_medusa + reserved_shopify
        
        -- Calculate new available
        local new_available = math.max(0, erp_qty - total_reserved)
        
        -- Update hash
        redis.call('HSET', key, 'erp_quantity', erp_qty)
        redis.call('HSET', key, 'available', new_available)
        
        table.insert(results, {
            key = key,
            success = true,
            available = new_available
        })
    else
        table.insert(results, {
            key = key,
            success = false,
            reason = "invalid_quantity"
        })
    end
end

return cjson.encode(results)
"""

# GET_KIT_AVAILABILITY_SCRIPT
# Atomically reads all component availabilities and computes kit availability.
# Formula: min(floor(component_available / required_qty)) for all components
#
# KEYS = list of component keys (inv:{warehouse_id}:{child_sku})
# ARGV = list of required quantities per kit (same order as KEYS)
#
# Returns: number of complete kits that can be assembled
GET_KIT_AVAILABILITY_SCRIPT = """
local min_kits = nil

for i, key in ipairs(KEYS) do
    local available = tonumber(redis.call('HGET', key, 'available') or '0')
    local required = tonumber(ARGV[i])
    
    if required and required > 0 then
        local kits_from_this = math.floor(available / required)
        if min_kits == nil or kits_from_this < min_kits then
            min_kits = kits_from_this
        end
    end
end

if min_kits == nil then
    return 0
end
return min_kits
"""

# RESERVE_KIT_SCRIPT
# Atomically reserves inventory for a kit by reserving all component SKUs.
# First checks availability of all components, then reserves all or none.
#
# KEYS = list of component keys (inv:{warehouse_id}:{child_sku})
# ARGV[1] = source ("medusa" or "shopify")
# ARGV[2..n] = quantities to reserve for each component (same order as KEYS)
#
# Returns: JSON with success status and component states
RESERVE_KIT_SCRIPT = """
local source = ARGV[1]
local reserved_key = 'reserved_' .. source
local num_components = #KEYS

-- Phase 1: Check all components have sufficient availability
local component_states = {}
for i, key in ipairs(KEYS) do
    local qty = tonumber(ARGV[i + 1])  -- +1 because ARGV[1] is source
    local available = tonumber(redis.call('HGET', key, 'available') or '0')
    
    if qty <= 0 then
        return cjson.encode({
            success = false,
            reason = "invalid_quantity",
            message = "Component quantity must be positive",
            component_index = i
        })
    end
    
    if available < qty then
        return cjson.encode({
            success = false,
            reason = "insufficient_stock",
            component_key = key,
            component_index = i,
            available = available,
            requested = qty,
            source = source
        })
    end
    
    table.insert(component_states, {
        key = key,
        qty = qty,
        available_before = available
    })
end

-- Phase 2: All checks passed, atomically reserve all components
local results = {}
for i, state in ipairs(component_states) do
    redis.call('HINCRBY', state.key, 'available', -state.qty)
    redis.call('HINCRBY', state.key, reserved_key, state.qty)
    
    local new_available = tonumber(redis.call('HGET', state.key, 'available'))
    local new_reserved = tonumber(redis.call('HGET', state.key, reserved_key))
    
    table.insert(results, {
        key = state.key,
        quantity_reserved = state.qty,
        available = new_available,
        reserved = new_reserved
    })
end

return cjson.encode({
    success = true,
    source = source,
    components_reserved = #results,
    components = results
})
"""

# RELEASE_KIT_SCRIPT
# Atomically releases reserved inventory for all components of a kit.
# Used for kit cancellation or fulfillment.
#
# KEYS = list of component keys (inv:{warehouse_id}:{child_sku})
# ARGV[1] = source ("medusa" or "shopify")
# ARGV[2] = reason ("fulfilled", "cancelled", "expired")
# ARGV[3..n] = quantities to release for each component (same order as KEYS)
#
# Returns: JSON with success status and component states
RELEASE_KIT_SCRIPT = """
local source = ARGV[1]
local reason = ARGV[2] or 'cancelled'
local reserved_key = 'reserved_' .. source

local results = {}
for i, key in ipairs(KEYS) do
    local qty = tonumber(ARGV[i + 2])  -- +2 because ARGV[1] is source, ARGV[2] is reason
    local current_reserved = tonumber(redis.call('HGET', key, reserved_key) or '0')
    
    -- Can't release more than reserved
    local release_qty = math.min(qty, current_reserved)
    
    if release_qty > 0 then
        -- Always decrement reserved count
        redis.call('HINCRBY', key, reserved_key, -release_qty)
        
        -- Handle based on reason
        -- 'fulfilled' and 'erp_handoff' both mean item is leaving inventory
        if reason == 'fulfilled' or reason == 'erp_handoff' then
            -- Fulfillment/ERP handoff: item shipped, decrement erp_quantity
            redis.call('HINCRBY', key, 'erp_quantity', -release_qty)
        else
            -- Cancellation: item returns to available stock
            redis.call('HINCRBY', key, 'available', release_qty)
        end
    end
    
    local new_available = tonumber(redis.call('HGET', key, 'available'))
    local new_reserved = tonumber(redis.call('HGET', key, reserved_key))
    
    table.insert(results, {
        key = key,
        released = release_qty,
        available = new_available,
        reserved = new_reserved
    })
end

return cjson.encode({
    success = true,
    source = source,
    reason = reason,
    components_released = #results,
    components = results
})
"""
