-- cast_vote.lua
-- Atomic vote casting with 7-ticket system
--
-- Business Rules:
-- 1. Each user has 7 distinct tickets (creative, visual, etc.)
-- 2. Each ticket can only be assigned to ONE program
-- 3. A user can only give ONE ticket to each program (no multi-ticket to same program)
-- 4. Supports revoke: if user already used this ticket, revoke old vote first
--
-- KEYS[1] = vote:user:{userId}:tickets (Hash)
-- KEYS[2] = vote:user:{userId}:programs (Set)
-- KEYS[3] = vote:count:{programId}:{ticketType} (String counter)
-- KEYS[4] = vote:leaderboard:{ticketType} (Sorted Set)
-- KEYS[5] = vote:program:{programId}:voters (Set)
-- KEYS[6] = vote:sync:queue (List)
-- KEYS[7] = vote:lock:{userId} (String)
--
-- ARGV[1] = userId
-- ARGV[2] = programId
-- ARGV[3] = ticketType
-- ARGV[4] = timestamp
-- ARGV[5] = lockTtlMs

local user_tickets_key = KEYS[1]
local user_programs_key = KEYS[2]
local count_key = KEYS[3]
local leaderboard_key = KEYS[4]
local program_voters_key = KEYS[5]
local sync_queue_key = KEYS[6]
local lock_key = KEYS[7]

local user_id = ARGV[1]
local program_id = ARGV[2]
local ticket_type = ARGV[3]
local timestamp = ARGV[4]
local lock_ttl = tonumber(ARGV[5])

-- Step 1: Acquire distributed lock
local lock_acquired = redis.call('SET', lock_key, timestamp, 'NX', 'PX', lock_ttl)
if not lock_acquired then
    return cjson.encode({
        success = false,
        error = 'LOCK_FAILED',
        message = 'Another vote operation in progress'
    })
end

-- Step 2: Check if user already voted for this program with ANY ticket
local already_voted_program = redis.call('SISMEMBER', user_programs_key, program_id)
local current_ticket_program = redis.call('HGET', user_tickets_key, ticket_type)

-- Case: User trying to vote same program with same ticket (no-op)
if current_ticket_program == program_id then
    redis.call('DEL', lock_key)
    return cjson.encode({
        success = true,
        message = 'Already voted for this program with this ticket',
        program_id = program_id,
        ticket_type = ticket_type,
        is_duplicate = true
    })
end

-- Case: User already voted for this program with a DIFFERENT ticket
if already_voted_program == 1 and current_ticket_program ~= program_id then
    redis.call('DEL', lock_key)
    return cjson.encode({
        success = false,
        error = 'ALREADY_VOTED_PROGRAM',
        message = 'You already voted for this program with another ticket'
    })
end

-- Step 3: If this ticket was used before, revoke the old vote
local old_program_id = current_ticket_program
local revoked = false

if old_program_id and old_program_id ~= false then
    -- Decrement old program's count
    local old_count_key = 'vote:count:' .. old_program_id .. ':' .. ticket_type
    local old_leaderboard_key = 'vote:leaderboard:' .. ticket_type
    local old_voters_key = 'vote:program:' .. old_program_id .. ':voters'

    redis.call('DECR', old_count_key)
    redis.call('ZINCRBY', old_leaderboard_key, -1, old_program_id)
    redis.call('SREM', old_voters_key, user_id)
    redis.call('SREM', user_programs_key, old_program_id)

    revoked = true
end

-- Step 4: Cast the new vote
-- 4a: Set the ticket assignment
redis.call('HSET', user_tickets_key, ticket_type, program_id)

-- 4b: Add program to user's voted programs
redis.call('SADD', user_programs_key, program_id)

-- 4c: Increment vote count
local new_count = redis.call('INCR', count_key)

-- 4d: Update leaderboard
redis.call('ZINCRBY', leaderboard_key, 1, program_id)

-- 4e: Add user to program's voters
redis.call('SADD', program_voters_key, user_id)

-- Step 5: Queue for MySQL sync
local vote_record = cjson.encode({
    action = 'cast',
    user_id = user_id,
    program_id = program_id,
    ticket_type = ticket_type,
    timestamp = timestamp,
    revoked_program = old_program_id or nil
})
redis.call('RPUSH', sync_queue_key, vote_record)

-- Step 6: Release lock
redis.call('DEL', lock_key)

return cjson.encode({
    success = true,
    program_id = program_id,
    ticket_type = ticket_type,
    new_count = new_count,
    revoked = revoked,
    revoked_program = old_program_id or nil
})
