// src/lib/tab-lock.ts
// Copyright (C) 2026 Robert Colbert <rob.colbert@openplatform.us>
// All Rights Reserved

import redis from './redis.js';
import { GadgetLog } from '@gadget/api';

const log = new GadgetLog({ name: 'TabLock', slug: 'tab-lock' });

const LOCK_KEY = (chatSessionId: string) => `gadget:lock:${chatSessionId}`;
const LOCK_TIMEOUT_MS = 60000; // 1 minute - must be renewed by heartbeat

export interface TabLockInfo {
  socketId: string;
  userId: string;
  acquiredAt: number;
}

class TabLockService {
  /**
   * Attempts to acquire a tab lock for a chat session.
   * Returns success=true if lock acquired, false if already locked by another tab.
   */
  async acquire(
    chatSessionId: string,
    userId: string,
    socketId: string
  ): Promise<{ success: boolean; info?: TabLockInfo }> {
    const key = LOCK_KEY(chatSessionId);
    const now = Date.now();
    
    try {
      // Try to set lock with NX (only if not exists)
      const acquired = await redis.set(
        key,
        JSON.stringify({ socketId, userId, acquiredAt: now }),
        'EX',
        LOCK_TIMEOUT_MS / 1000,
        'NX'
      );
      
      if (acquired === 'OK') {
        log.info('tab lock acquired', { chatSessionId, userId, socketId });
        return { success: true };
      }
      
      // Lock exists - check if it's ours or stale
      const existing = await redis.get(key);
      if (!existing) {
        // Lock disappeared between check and set - retry
        return this.acquire(chatSessionId, userId, socketId);
      }
      
      const info = JSON.parse(existing) as TabLockInfo;
      
      // Check if lock is stale (expired but not cleaned up)
      if (now - info.acquiredAt > LOCK_TIMEOUT_MS) {
        log.info('stale tab lock detected, reclaiming', { chatSessionId, info });
        await this.release(chatSessionId);
        return this.acquire(chatSessionId, userId, socketId);
      }
      
      // Lock is held by another tab
      log.info('tab lock denied - already locked', { 
        chatSessionId, 
        lockedBy: info.socketId,
        ourSocket: socketId 
      });
      
      return { success: false, info };
    } catch (error) {
      log.error('failed to acquire tab lock', { chatSessionId, error });
      throw error;
    }
  }
  
  /**
   * Releases a tab lock.
   */
  async release(chatSessionId: string, socketId?: string): Promise<void> {
    const key = LOCK_KEY(chatSessionId);
    
    try {
      if (socketId) {
        // Only release if we hold the lock
        const existing = await redis.get(key);
        if (existing) {
          const info = JSON.parse(existing) as TabLockInfo;
          if (info.socketId === socketId) {
            await redis.del(key);
            log.info('tab lock released', { chatSessionId, socketId });
          }
        }
      } else {
        await redis.del(key);
      }
    } catch (error) {
      log.error('failed to release tab lock', { chatSessionId, error });
    }
  }
  
  /**
   * Refreshes the TTL on an existing lock (heartbeat).
   */
  async refresh(chatSessionId: string): Promise<boolean> {
    const key = LOCK_KEY(chatSessionId);
    
    try {
      const refreshed = await redis.expire(key, LOCK_TIMEOUT_MS / 1000);
      return refreshed !== 0;
    } catch (error) {
      log.error('failed to refresh tab lock', { chatSessionId, error });
      return false;
    }
  }
  
  /**
   * Gets current lock info without modifying.
   */
  async getInfo(chatSessionId: string): Promise<TabLockInfo | null> {
    const key = LOCK_KEY(chatSessionId);
    
    try {
      const existing = await redis.get(key);
      if (!existing) return null;
      return JSON.parse(existing) as TabLockInfo;
    } catch (error) {
      log.error('failed to get tab lock info', { chatSessionId, error });
      return null;
    }
  }
}

export default new TabLockService();
