/**
 * Chrome's Hammer - SOCKS5 Proxy with Bidirectional Tunneling
 * Supports both HTTP and HTTPS through true SOCKS5 tunneling
 */

// Configuration
var PROXY_HOST = '127.0.0.1';
var PROXY_PORT = 1080;

// TCP Server Management
var serverSocketId = null;
var isListening = false;
var isInitialized = false;  // Prevent double initialization
var handlersSetup = false;  // Prevent duplicate event listeners
var appWindow = null;  // Keep app window reference to prevent suspension

// Connection statistics
var stats = {
  totalConnections: 0,
  activeConnections: 0,
  bytesReceived: 0,
  bytesSent: 0,
  startTime: Date.now(),
  connections: {}  // socketId -> { connectTime, bytesReceived, bytesSent }
};

// SOCKS5 Connection States
var ConnectionState = {
  GREETING: 'greeting',
  REQUEST: 'request',
  TUNNEL: 'tunnel',  // NEW: Tunneling state
  CLOSED: 'closed'
};

// Track connections: clientSocketId -> { state, buffer, destinationSocketId }
var connections = {};

// Track reverse mapping: destinationSocketId -> clientSocketId
var reverseConnections = {};

// ============================================================================
// TCP SERVER MANAGEMENT
// ============================================================================

function createServer(host, port, callback) {
  chrome.sockets.tcpServer.create({}, function(createInfo) {
    if (chrome.runtime.lastError) {
      callback(chrome.runtime.lastError, null);
      return;
    }

    serverSocketId = createInfo.socketId;
    console.log('[TCP Server] Created server socket: ' + serverSocketId);

    chrome.sockets.tcpServer.listen(serverSocketId, host, port, function(result) {
      if (result < 0) {
        callback(new Error('Failed to listen on ' + host + ':' + port + ', code: ' + result), null);
        return;
      }

      isListening = true;
      console.log('[TCP Server] Listening on ' + host + ':' + port);
      callback(null, serverSocketId);
    });
  });
}

// ============================================================================
// SOCKS5 PROTOCOL HANDLER
// ============================================================================

// SOCKS5 constants
var SOCKS_VERSION = 0x05;
var SOCKS_CMD_CONNECT = 0x01;
var SOCKS_ATYP_IPV4 = 0x01;
var SOCKS_ATYP_DOMAIN = 0x03;
var SOCKS_ATYP_IPV6 = 0x04;
var SOCKS_AUTH_NONE = 0x00;
var SOCKS_REPLY_SUCCESS = 0x00;
var SOCKS_REPLY_GENERAL_FAILURE = 0x01;
var SOCKS_REPLY_CONNECTION_NOT_ALLOWED = 0x02;
var SOCKS_REPLY_NETWORK_UNREACHABLE = 0x03;
var SOCKS_REPLY_HOST_UNREACHABLE = 0x04;
var SOCKS_REPLY_CONNECTION_REFUSED = 0x05;

function initConnection(socketId) {
  connections[socketId] = {
    state: ConnectionState.GREETING,
    buffer: new Uint8Array(0),
    destinationSocketId: null
  };

  // Track statistics
  stats.totalConnections++;
  stats.activeConnections++;
  stats.connections[socketId] = {
    connectTime: Date.now(),
    bytesReceived: 0,
    bytesSent: 0
  };

  console.log('[SOCKS] Initialized connection ' + socketId);
  sendStatusToWindow();
}

function removeConnection(socketId) {
  var conn = connections[socketId];
  if (conn && conn.destinationSocketId) {
    delete reverseConnections[conn.destinationSocketId];
    try {
      chrome.sockets.tcp.close(conn.destinationSocketId);
    } catch(e) {}
  }
  delete connections[socketId];

  // Update statistics
  if (stats.connections[socketId]) {
    delete stats.connections[socketId];
  }
  stats.activeConnections--;

  console.log('[SOCKS] Removed connection ' + socketId);
  sendStatusToWindow();
}

function handleSocksData(clientSocketId, data) {
  var conn = connections[clientSocketId];
  if (!conn) {
    console.error('[SOCKS] No connection state for ' + clientSocketId);
    return;
  }

  // If we're in tunnel mode, forward data to destination
  if (conn.state === ConnectionState.TUNNEL && conn.destinationSocketId) {
    var byteLength = data.byteLength;
    console.log('[Tunnel] Client -> Destination: ' + byteLength + ' bytes');

    // Track statistics
    stats.bytesReceived += byteLength;
    if (stats.connections[clientSocketId]) {
      stats.connections[clientSocketId].bytesReceived += byteLength;
    }

    chrome.sockets.tcp.send(conn.destinationSocketId, data, function(sendInfo) {
      if (sendInfo.resultCode < 0) {
        console.error('[Tunnel] Failed to send to destination: ' + sendInfo.resultCode);
        closeConnection(clientSocketId);
        return;
      }
      // Track sent bytes
      stats.bytesSent += byteLength;
      if (stats.connections[clientSocketId]) {
        stats.connections[clientSocketId].bytesSent += byteLength;
      }
    });
    return;
  }

  // Append new data to buffer
  var newData = new Uint8Array(data);
  var newBuffer = new Uint8Array(conn.buffer.length + newData.length);
  newBuffer.set(conn.buffer);
  newBuffer.set(newData, conn.buffer.length);
  conn.buffer = newBuffer;

  // Process based on current state
  if (conn.state === ConnectionState.GREETING) {
    handleGreeting(clientSocketId, conn);
  } else if (conn.state === ConnectionState.REQUEST) {
    handleRequest(clientSocketId, conn);
  }
}

function handleGreeting(socketId, conn) {
  var data = conn.buffer;

  if (data.length < 3) {
    return; // Need more data
  }

  var version = data[0];
  var numMethods = data[1];

  if (version !== SOCKS_VERSION) {
    console.error('[SOCKS] Invalid SOCKS version: ' + version);
    closeConnection(socketId);
    return;
  }

  if (data.length < 2 + numMethods) {
    return; // Need more data
  }

  // Check if no authentication (method 0x00) is supported
  var methodSupported = false;
  for (var i = 0; i < numMethods; i++) {
    if (data[2 + i] === SOCKS_AUTH_NONE) {
      methodSupported = true;
      break;
    }
  }

  if (methodSupported) {
    sendGreetingResponse(socketId, SOCKS_AUTH_NONE);
    conn.state = ConnectionState.REQUEST;
    // Clear buffer after greeting
    conn.buffer = new Uint8Array(0);
  } else {
    closeConnection(socketId);
  }
}

function sendGreetingResponse(socketId, method) {
  var response = new Uint8Array([SOCKS_VERSION, method]);
  chrome.sockets.tcp.send(socketId, response.buffer, function(sendInfo) {
    if (sendInfo.resultCode < 0) {
      console.error('[SOCKS] Failed to send greeting response: ' + sendInfo.resultCode);
    } else {
      console.log('[SOCKS] Sent greeting response, method: ' + method);
    }
  });
}

function handleRequest(clientSocketId, conn) {
  var data = conn.buffer;

  if (data.length < 4) {
    return; // Need more data
  }

  var version = data[0];
  var command = data[1];
  var atyp = data[3];

  if (version !== SOCKS_VERSION) {
    console.error('[SOCKS] Invalid SOCKS version in request: ' + version);
    sendRequestResponse(clientSocketId, SOCKS_REPLY_GENERAL_FAILURE);
    closeConnection(clientSocketId);
    return;
  }

  if (command !== SOCKS_CMD_CONNECT) {
    console.error('[SOCKS] Unsupported command: ' + command);
    sendRequestResponse(clientSocketId, SOCKS_REPLY_COMMAND_NOT_SUPPORTED);
    closeConnection(clientSocketId);
    return;
  }

  // Parse address
  var host = '';
  var port = 0;
  var consumed = 0;

  if (atyp === SOCKS_ATYP_IPV4) {
    if (data.length < 10) {
      return; // Need more data
    }
    host = data[4] + '.' + data[5] + '.' + data[6] + '.' + data[7];
    port = (data[8] << 8) | data[9];
    consumed = 10;
  } else if (atyp === SOCKS_ATYP_DOMAIN) {
    var domainLen = data[4];
    if (data.length < 7 + domainLen) {
      return; // Need more data
    }
    host = '';
    for (var i = 0; i < domainLen; i++) {
      host += String.fromCharCode(data[5 + i]);
    }
    port = (data[5 + domainLen] << 8) | data[6 + domainLen];
    consumed = 7 + domainLen;
  } else if (atyp === SOCKS_ATYP_IPV6) {
    if (data.length < 22) {
      return; // Need more data
    }
    // Parse IPv6 (simplified)
    var parts = [];
    for (var j = 0; j < 16; j += 2) {
      parts.push(((data[4 + j] << 8) | data[5 + j]).toString(16));
    }
    host = parts.join(':');
    port = (data[20] << 8) | data[21];
    consumed = 22;
  } else {
    console.error('[SOCKS] Unsupported address type: ' + atyp);
    sendRequestResponse(clientSocketId, SOCKS_REPLY_ADDRESS_TYPE_NOT_SUPPORTED);
    closeConnection(clientSocketId);
    return;
  }

  console.log('[SOCKS] CONNECT request: ' + host + ':' + port);

  // Create tunnel to destination
  createTunnel(clientSocketId, host, port, consumed);
}

function createTunnel(clientSocketId, host, port, consumed) {
  // Clear the request buffer
  var conn = connections[clientSocketId];
  conn.buffer = new Uint8Array(0);

  // Create TCP connection to destination
  chrome.sockets.tcp.create({}, function(createInfo) {
    if (chrome.runtime.lastError || !createInfo) {
      console.error('[Tunnel] Failed to create socket:', chrome.runtime.lastError);
      sendRequestResponse(clientSocketId, SOCKS_REPLY_GENERAL_FAILURE);
      closeConnection(clientSocketId);
      return;
    }

    var destSocketId = createInfo.socketId;
    conn.destinationSocketId = destSocketId;
    reverseConnections[destSocketId] = clientSocketId;

    console.log('[Tunnel] Created destination socket: ' + destSocketId);

    // Connect to destination
    chrome.sockets.tcp.connect(destSocketId, host, port, function(result) {
      if (result < 0) {
        console.error('[Tunnel] Failed to connect to ' + host + ':' + port + ', code: ' + result);

        // Map error codes
        var reply = SOCKS_REPLY_GENERAL_FAILURE;
        if (result === -109) { // Host unreachable
          reply = SOCKS_REPLY_HOST_UNREACHABLE;
        } else if (result === -111) { // Connection refused
          reply = SOCKS_REPLY_CONNECTION_REFUSED;
        } else if (result === -102) { // Network unreachable
          reply = SOCKS_REPLY_NETWORK_UNREACHABLE;
        }

        sendRequestResponse(clientSocketId, reply);
        closeConnection(clientSocketId);
        return;
      }

      console.log('[Tunnel] Connected to ' + host + ':' + port);

      // Send success response to client
      sendRequestResponse(clientSocketId, SOCKS_REPLY_SUCCESS);

      // Set to tunnel mode
      conn.state = ConnectionState.TUNNEL;

      // Unpause destination socket to start receiving data
      chrome.sockets.tcp.setPaused(destSocketId, false);

      console.log('[Tunnel] Tunnel established: client=' + clientSocketId + ' -> dest=' + destSocketId);
    });
  });
}

function sendRequestResponse(socketId, reply) {
  var response = new Uint8Array([
    SOCKS_VERSION,      // VER
    reply,              // REP
    0x00,               // RSV
    SOCKS_ATYP_IPV4,    // ATYP
    0x00, 0x00, 0x00, 0x00, // BND.ADDR
    0x00, 0x00          // BND.PORT
  ]);

  chrome.sockets.tcp.send(socketId, response.buffer, function(sendInfo) {
    if (sendInfo.resultCode < 0) {
      console.error('[SOCKS] Failed to send request response: ' + sendInfo.resultCode);
    } else {
      console.log('[SOCKS] Sent request response, reply: ' + reply);
    }
  });
}

function closeConnection(socketId) {
  chrome.sockets.tcp.close(socketId);
  removeConnection(socketId);
}

// ============================================================================
// BIDIRECTIONAL TUNNEL DATA FORWARDING
// ============================================================================

// Handle data from destination - forward to client
function handleDestinationData(destSocketId, data) {
  var clientSocketId = reverseConnections[destSocketId];

  if (!clientSocketId) {
    console.warn('[Tunnel] No client for destination socket: ' + destSocketId);
    return;
  }

  var byteLength = data.byteLength;
  console.log('[Tunnel] Destination -> Client: ' + byteLength + ' bytes');

  // Track statistics
  stats.bytesReceived += byteLength;
  if (stats.connections[clientSocketId]) {
    stats.connections[clientSocketId].bytesReceived += byteLength;
  }

  // Pause destination to prevent event spam while sending
  chrome.sockets.tcp.setPaused(destSocketId, true);

  chrome.sockets.tcp.send(clientSocketId, data, function(sendInfo) {
    // Resume destination after sending
    chrome.sockets.tcp.setPaused(destSocketId, false);

    if (sendInfo.resultCode < 0) {
      console.error('[Tunnel] Failed to send to client: ' + sendInfo.resultCode);
      // Close both connections
      closeConnection(clientSocketId);
      return;
    }
    // Track sent bytes
    stats.bytesSent += byteLength;
    if (stats.connections[clientSocketId]) {
      stats.connections[clientSocketId].bytesSent += byteLength;
    }
  });
}

// ============================================================================
// MAIN INITIALIZATION
// ============================================================================

function setupConnectionHandlers() {
  // Handle new connections from clients
  chrome.sockets.tcpServer.onAccept.addListener(function(acceptInfo) {
    console.log('[Hammer] New client connected: ' + acceptInfo.clientSocketId);

    // Initialize SOCKS state for this connection
    initConnection(acceptInfo.clientSocketId);

    // Unpause socket to start receiving data
    chrome.sockets.tcp.setPaused(acceptInfo.clientSocketId, false);
  });

  // Handle accept errors
  chrome.sockets.tcpServer.onAcceptError.addListener(function(errorInfo) {
    console.error('[Hammer] Accept error:', errorInfo);
  });

  // Handle receive errors
  chrome.sockets.tcp.onReceiveError.addListener(function(info) {
    var socketId = info.socketId;
    var resultCode = info.resultCode;

    // Error code -100 is ERR_CONNECTION_CLOSED - normal disconnect, not an error
    if (resultCode === -100) {
      console.log('[Hammer] Socket ' + socketId + ' connection closed');
    } else {
      console.error('[Hammer] Receive error on socket ' + socketId + ': ' + resultCode);
    }

    // Clean up connections
    if (connections[socketId]) {
      closeConnection(socketId);
    } else if (reverseConnections[socketId]) {
      closeConnection(reverseConnections[socketId]);
    }
  });

  // Handle disconnects (EOF detection - zero byte receive)
  chrome.sockets.tcp.onReceive.addListener(function(receiveInfo) {
    if (receiveInfo.data.byteLength === 0) {
      var socketId = receiveInfo.socketId;
      console.log('[Hammer] Socket ' + socketId + ' disconnected (EOF)');
      if (connections[socketId]) {
        closeConnection(socketId);
      } else if (reverseConnections[socketId]) {
        closeConnection(reverseConnections[socketId]);
      }
      return;
    }

    // If not EOF, let the main onReceive handler handle the data
    var socketId = receiveInfo.socketId;

    // Check if this is data from a client or destination
    if (connections[socketId]) {
      // Data from client
      console.log('[Hammer] Received from client ' + socketId + ': ' + receiveInfo.data.byteLength + ' bytes');
      handleSocksData(socketId, receiveInfo.data);
    } else if (reverseConnections[socketId]) {
      // Data from destination
      handleDestinationData(socketId, receiveInfo.data);
    } else {
      console.warn('[Hammer] Received data from unknown socket: ' + socketId);
    }
  });
}

function initializeProxy() {
  // Prevent double initialization
  if (isInitialized) {
    console.log('[Hammer] Already initialized or initializing...');
    return;
  }
  isInitialized = true;

  console.log('[Hammer] Initializing Chrome\'s Hammer (with tunneling)...');

  createServer(PROXY_HOST, PROXY_PORT, function(error, socketId) {
    if (error) {
      console.error('[Hammer] Failed to initialize:', error);
      isInitialized = false;  // Allow retry on failure
      return;
    }

    console.log('[Hammer] Chrome\'s Hammer is running on ' + PROXY_HOST + ':' + PROXY_PORT);

    // Setup handlers only once
    if (!handlersSetup) {
      setupConnectionHandlers();
      handlersSetup = true;
    }
  });
}

// Chrome App lifecycle events
chrome.app.runtime.onLaunched.addListener(function() {
  console.log('[Hammer] App launched');
  initializeProxy();
  createAppWindow();
});

// Create a minimal app window to prevent Chrome from suspending the app
function createAppWindow() {
  // Only create if we don't already have one
  if (appWindow) {
    try {
      appWindow.focus();
      return;
    } catch(e) {
      appWindow = null;
    }
  }

  chrome.app.window.create('window.html', {
    'outerBounds': {
      'width': 400,
      'height': 500,
      'minWidth': 300,
      'minHeight': 400
    },
    'resizable': true,
    'alwaysOnTop': false
  }, function(createdWindow) {
    if (chrome.runtime.lastError) {
      console.error('[Hammer] Failed to create window:', chrome.runtime.lastError);
      return;
    }
    appWindow = createdWindow;
    console.log('[Hammer] App window created - prevents suspension');

    // Re-create window if closed
    appWindow.onClosed.addListener(function() {
      console.log('[Hammer] Window closed, recreating in 2 seconds...');
      appWindow = null;
      setTimeout(function() {
        if (!appWindow) {
          createAppWindow();
        }
      }, 2000);
    });
  });
}

// Chrome App lifecycle - restart when app is restarted
chrome.app.runtime.onRestarted.addListener(function() {
  console.log('[Hammer] App restarted');
  // Reset state and reinitialize
  isListening = false;
  isInitialized = false;
  serverSocketId = null;
  initializeProxy();
});

// Handle the app window being closed
chrome.app.window.onClosed.addListener(function() {
  console.log('[Hammer] App window closed');
  // Clean up connections
  for (var socketId in connections) {
    try {
      chrome.sockets.tcp.close(parseInt(socketId));
    } catch(e) {}
  }
  connections = {};
  reverseConnections = {};

  // Close server socket
  if (serverSocketId !== null) {
    try {
      chrome.sockets.tcpServer.close(serverSocketId);
    } catch(e) {}
    serverSocketId = null;
  }
  isListening = false;
  isInitialized = false;
});

// Also initialize on startup (for apps that are already running)
initializeProxy();

// Create window on startup to prevent suspension
setTimeout(function() {
  if (!appWindow) {
    createAppWindow();
  }
}, 1000);

// ============================================================================
// KEEP-ALIVE MECHANISM
// ============================================================================

// Setup keep-alive alarm to prevent Chrome from suspending the app
chrome.alarms.create('keepAlive', { periodInMinutes: 1 });

chrome.alarms.onAlarm.addListener(function(alarm) {
  if (alarm.name === 'keepAlive') {
    // Heartbeat: log activity to keep the app alive
    console.log('[Hammer] Heartbeat - Proxy is ' + (isListening ? 'active' : 'inactive'));

    // Send status to window
    sendStatusToWindow();

    // Check if server is still listening, restart if needed
    if (!isListening && !isInitialized) {
      console.log('[Hammer] Server not listening, reinitializing...');
      initializeProxy();
    }

    // Request keep awake to prevent system sleep (optional)
    chrome.power.requestKeepAwake('system');
  }
});

// Send status to app window
function sendStatusToWindow() {
  // Format bytes for display
  function formatBytes(bytes) {
    if (bytes < 1024) return bytes + ' B';
    if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
    return (bytes / (1024 * 1024)).toFixed(2) + ' MB';
  }

  // Format duration for display
  function formatDuration(ms) {
    var seconds = Math.floor(ms / 1000);
    var minutes = Math.floor(seconds / 60);
    var hours = Math.floor(minutes / 60);
    if (hours > 0) return hours + 'h ' + (minutes % 60) + 'm';
    if (minutes > 0) return minutes + 'm ' + (seconds % 60) + 's';
    return seconds + 's';
  }

  // Calculate connection durations
  var connectionDurations = [];
  for (var socketId in stats.connections) {
    var conn = stats.connections[socketId];
    var duration = Date.now() - conn.connectTime;
    connectionDurations.push(formatDuration(duration));
  }

  chrome.runtime.sendMessage({
    type: 'status',
    active: isListening,
    stats: {
      totalConnections: stats.totalConnections,
      activeConnections: stats.activeConnections,
      bytesReceived: formatBytes(stats.bytesReceived),
      bytesSent: formatBytes(stats.bytesSent),
      uptime: formatDuration(Date.now() - stats.startTime),
      connectionDurations: connectionDurations
    }
  });
  chrome.runtime.sendMessage({
    type: 'heartbeat'
  });
}

// Listen for status requests from window
chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) {
  if (message.type === 'getStatus') {
    sendResponse({
      type: 'status',
      active: isListening,
      stats: {
        totalConnections: stats.totalConnections,
        activeConnections: stats.activeConnections
      }
    });
  }
});

// Handle when app is suspended and resumed
chrome.runtime.onSuspend.addListener(function() {
  console.log('[Hammer] App being suspended...');
});

chrome.runtime.onSuspendCanceled.addListener(function() {
  console.log('[Hammer] Suspend canceled - app is active again');

  // Reinitialize if needed after unsuspend
  if (!isListening) {
    console.log('[Hammer] Reinitializing after unsuspend...');
    isInitialized = false;
    initializeProxy();
  }
});

console.log('[Hammer] Chrome\'s Hammer background script loaded');
