const http = require('http')
const fs = require('fs')
const path = require('path')
const WebSocket = require('ws')
const { WebSocketServer } = WebSocket

const HOST = '0.0.0.0'
const PORT = 17865
const GPS_WS_PATH = '/mock-gps'
const HEART_RATE_WS_PATH = '/mock-hr'
const DEBUG_LOG_WS_PATH = '/debug-log'
const PROXY_PATH = '/proxy'
const BRIDGE_STATUS_PATH = '/bridge-status'
const BRIDGE_CONFIG_PATH = '/bridge-config'
const PUBLIC_DIR = path.join(__dirname, 'public')
const DEFAULT_GATEWAY_BRIDGE_URL = 'ws://127.0.0.1:18080/ws'

const INITIAL_BRIDGE_CONFIG = {
  enabled: process.env.MOCK_SIM_GATEWAY_ENABLED === '1',
  url: process.env.MOCK_SIM_GATEWAY_URL || DEFAULT_GATEWAY_BRIDGE_URL,
  token: process.env.MOCK_SIM_GATEWAY_TOKEN || 'dev-producer-token',
  channelId: process.env.MOCK_SIM_GATEWAY_CHANNEL_ID || '',
  deviceId: process.env.MOCK_SIM_GATEWAY_DEVICE_ID || 'child-001',
  groupId: process.env.MOCK_SIM_GATEWAY_GROUP_ID || '',
  sourceId: process.env.MOCK_SIM_GATEWAY_SOURCE_ID || 'mock-gps-sim',
  sourceMode: process.env.MOCK_SIM_GATEWAY_SOURCE_MODE || 'mock',
  reconnectMs: Math.max(1000, Number(process.env.MOCK_SIM_GATEWAY_RECONNECT_MS) || 3000),
}

function getContentType(filePath) {
  const ext = path.extname(filePath).toLowerCase()
  if (ext === '.html') {
    return 'text/html; charset=utf-8'
  }
  if (ext === '.css') {
    return 'text/css; charset=utf-8'
  }
  if (ext === '.js') {
    return 'application/javascript; charset=utf-8'
  }
  if (ext === '.json') {
    return 'application/json; charset=utf-8'
  }
  if (ext === '.svg') {
    return 'image/svg+xml'
  }
  return 'text/plain; charset=utf-8'
}

function respondJson(response, statusCode, payload) {
  response.writeHead(statusCode, {
    'Content-Type': 'application/json; charset=utf-8',
    'Cache-Control': 'no-store',
    'Access-Control-Allow-Origin': '*',
  })
  response.end(JSON.stringify(payload))
}

function serveStatic(requestPath, response) {
  let safePath = requestPath === '/' ? '/index.html' : requestPath
  if (safePath.endsWith('/')) {
    safePath = `${safePath}index.html`
  } else if (!path.extname(safePath)) {
    safePath = `${safePath}/index.html`
  }
  const resolvedPath = path.normalize(path.join(PUBLIC_DIR, safePath))
  if (!resolvedPath.startsWith(PUBLIC_DIR)) {
    response.writeHead(403)
    response.end('Forbidden')
    return
  }

  fs.readFile(resolvedPath, (error, content) => {
    if (error) {
      response.writeHead(404)
      response.end('Not Found')
      return
    }

    response.writeHead(200, {
      'Content-Type': getContentType(resolvedPath),
      'Cache-Control': 'no-store',
    })
    response.end(content)
  })
}

function isMockGpsPayload(payload) {
  return payload
    && payload.type === 'mock_gps'
    && Number.isFinite(payload.lat)
    && Number.isFinite(payload.lon)
}

function isMockHeartRatePayload(payload) {
  return payload
    && payload.type === 'mock_heart_rate'
    && Number.isFinite(payload.bpm)
}

function isDebugLogPayload(payload) {
  return payload
    && payload.type === 'debug-log'
    && typeof payload.scope === 'string'
    && typeof payload.level === 'string'
    && typeof payload.message === 'string'
}

function normalizeChannelId(value) {
  const trimmed = String(value || '').trim()
  return trimmed || 'default'
}

async function handleProxyRequest(request, response) {
  const requestUrl = new URL(request.url || '/', `http://127.0.0.1:${PORT}`)
  const targetUrl = requestUrl.searchParams.get('url')
  if (!targetUrl) {
    response.writeHead(400, {
      'Content-Type': 'text/plain; charset=utf-8',
      'Access-Control-Allow-Origin': '*',
    })
    response.end('Missing url')
    return
  }

  try {
    const upstream = await fetch(targetUrl)
    const body = Buffer.from(await upstream.arrayBuffer())
    response.writeHead(upstream.status, {
      'Content-Type': upstream.headers.get('content-type') || 'application/octet-stream',
      'Cache-Control': 'no-store',
      'Access-Control-Allow-Origin': '*',
    })
    response.end(body)
  } catch (error) {
    response.writeHead(502, {
      'Content-Type': 'text/plain; charset=utf-8',
      'Access-Control-Allow-Origin': '*',
    })
    response.end(error && error.message ? error.message : 'Proxy request failed')
  }
}

async function readJsonBody(request) {
  return new Promise((resolve, reject) => {
    const chunks = []
    request.on('data', (chunk) => {
      chunks.push(chunk)
    })
    request.on('end', () => {
      const raw = Buffer.concat(chunks).toString('utf8').trim()
      if (!raw) {
        resolve({})
        return
      }
      try {
        resolve(JSON.parse(raw))
      } catch (error) {
        reject(error)
      }
    })
    request.on('error', reject)
  })
}

function normalizeBridgeConfig(input, currentConfig) {
  const source = input || {}
  const fallback = currentConfig || INITIAL_BRIDGE_CONFIG

  return {
    enabled: typeof source.enabled === 'boolean' ? source.enabled : fallback.enabled,
    url: typeof source.url === 'string' && source.url.trim() ? source.url.trim() : fallback.url,
    token: typeof source.token === 'string' ? source.token.trim() : fallback.token,
    channelId: typeof source.channelId === 'string' ? source.channelId.trim() : fallback.channelId,
    deviceId: typeof source.deviceId === 'string' && source.deviceId.trim() ? source.deviceId.trim() : fallback.deviceId,
    groupId: typeof source.groupId === 'string' ? source.groupId.trim() : fallback.groupId,
    sourceId: typeof source.sourceId === 'string' && source.sourceId.trim() ? source.sourceId.trim() : fallback.sourceId,
    sourceMode: typeof source.sourceMode === 'string' && source.sourceMode.trim() ? source.sourceMode.trim() : fallback.sourceMode,
    reconnectMs: Math.max(1000, Number(source.reconnectMs) || fallback.reconnectMs),
  }
}

function createGatewayBridge() {
  const bridgeState = {
    config: { ...INITIAL_BRIDGE_CONFIG },
    socket: null,
    connecting: false,
    connected: false,
    authenticated: false,
    reconnectTimer: 0,
    lastError: '',
    lastSentAt: 0,
    lastSentTopic: '',
    sentCount: 0,
    droppedCount: 0,
  }

  function logBridge(message) {
    console.log(`[gateway-bridge] ${message}`)
  }

  function clearReconnectTimer() {
    if (!bridgeState.reconnectTimer) {
      return
    }
    clearTimeout(bridgeState.reconnectTimer)
    bridgeState.reconnectTimer = 0
  }

  function scheduleReconnect() {
    if (!bridgeState.config.enabled || bridgeState.reconnectTimer) {
      return
    }
    bridgeState.reconnectTimer = setTimeout(() => {
      bridgeState.reconnectTimer = 0
      connect()
    }, bridgeState.config.reconnectMs)
  }

  function resetSocketState() {
    bridgeState.socket = null
    bridgeState.connecting = false
    bridgeState.connected = false
    bridgeState.authenticated = false
  }

  function handleGatewayMessage(rawMessage) {
    let parsed
    try {
      parsed = JSON.parse(String(rawMessage))
    } catch (_error) {
      return
    }

    if (parsed.type === 'welcome') {
      if (!bridgeState.socket || bridgeState.socket.readyState !== WebSocket.OPEN) {
        return
      }
      if (bridgeState.config.channelId) {
        bridgeState.socket.send(JSON.stringify({
          type: 'join_channel',
          role: 'producer',
          channelId: bridgeState.config.channelId,
          token: bridgeState.config.token,
        }))
      } else {
        bridgeState.socket.send(JSON.stringify({
          type: 'authenticate',
          role: 'producer',
          token: bridgeState.config.token,
        }))
      }
      return
    }

    if (parsed.type === 'authenticated' || parsed.type === 'joined_channel') {
      bridgeState.authenticated = true
      bridgeState.lastError = ''
      if (bridgeState.config.channelId) {
        logBridge(`joined channel=${bridgeState.config.channelId}, device=${bridgeState.config.deviceId}, source=${bridgeState.config.sourceId}`)
      } else {
        logBridge(`authenticated, device=${bridgeState.config.deviceId}, source=${bridgeState.config.sourceId}`)
      }
      return
    }

    if (parsed.type === 'error') {
      bridgeState.lastError = parsed.error || 'gateway error'
      logBridge(`error: ${bridgeState.lastError}`)
    }
  }

  function closeSocket() {
    if (!bridgeState.socket) {
      return
    }
    try {
      bridgeState.socket.close()
    } catch (_error) {
      // noop
    }
    resetSocketState()
  }

  function connect() {
    if (!bridgeState.config.enabled || bridgeState.connecting) {
      return
    }
    if (bridgeState.socket && (bridgeState.socket.readyState === WebSocket.OPEN || bridgeState.socket.readyState === WebSocket.CONNECTING)) {
      return
    }

    clearReconnectTimer()
    bridgeState.connecting = true
    bridgeState.lastError = ''
    logBridge(`connecting to ${bridgeState.config.url}`)

    const socket = new WebSocket(bridgeState.config.url)
    bridgeState.socket = socket

    socket.on('open', () => {
      bridgeState.connecting = false
      bridgeState.connected = true
      logBridge('connected')
    })

    socket.on('message', handleGatewayMessage)

    socket.on('close', () => {
      const wasConnected = bridgeState.connected || bridgeState.authenticated
      resetSocketState()
      if (wasConnected) {
        logBridge('disconnected')
      }
      scheduleReconnect()
    })

    socket.on('error', (error) => {
      bridgeState.lastError = error && error.message ? error.message : 'gateway socket error'
      logBridge(`socket error: ${bridgeState.lastError}`)
    })
  }

  function toGatewayEnvelope(payload) {
    if (isMockGpsPayload(payload)) {
      return {
        schemaVersion: 1,
        messageId: `gps-${payload.timestamp}`,
        timestamp: payload.timestamp,
        topic: 'telemetry.location',
        source: {
          kind: 'producer',
          id: bridgeState.config.sourceId,
          mode: bridgeState.config.sourceMode,
        },
        target: {
          channelId: bridgeState.config.channelId,
          deviceId: bridgeState.config.deviceId,
          groupId: bridgeState.config.groupId,
        },
        payload: {
          lat: Number(payload.lat),
          lng: Number(payload.lon),
          speed: Number(payload.speedMps) || 0,
          bearing: Number(payload.headingDeg) || 0,
          accuracy: Number(payload.accuracyMeters) || 6,
          coordSystem: 'GCJ02',
        },
      }
    }

    if (isMockHeartRatePayload(payload)) {
      return {
        schemaVersion: 1,
        messageId: `hr-${payload.timestamp}`,
        timestamp: payload.timestamp,
        topic: 'telemetry.heart_rate',
        source: {
          kind: 'producer',
          id: bridgeState.config.sourceId,
          mode: bridgeState.config.sourceMode,
        },
        target: {
          channelId: bridgeState.config.channelId,
          deviceId: bridgeState.config.deviceId,
          groupId: bridgeState.config.groupId,
        },
        payload: {
          bpm: Math.max(1, Math.round(Number(payload.bpm))),
        },
      }
    }

    return null
  }

  function publish(payload) {
    if (!bridgeState.config.enabled) {
      return
    }

    if (!bridgeState.socket || bridgeState.socket.readyState !== WebSocket.OPEN || !bridgeState.authenticated) {
      bridgeState.droppedCount += 1
      connect()
      return
    }

    const envelope = toGatewayEnvelope(payload)
    if (!envelope) {
      return
    }

    bridgeState.socket.send(JSON.stringify({
      type: 'publish',
      envelope,
    }))
    bridgeState.lastSentAt = Date.now()
    bridgeState.lastSentTopic = envelope.topic
    bridgeState.sentCount += 1
  }

  function updateConfig(nextConfigInput) {
    const nextConfig = normalizeBridgeConfig(nextConfigInput, bridgeState.config)
    const changed = JSON.stringify(nextConfig) !== JSON.stringify(bridgeState.config)
    bridgeState.config = nextConfig

    if (!changed) {
      return getStatus()
    }

    bridgeState.lastError = ''
    if (!bridgeState.config.enabled) {
      clearReconnectTimer()
      closeSocket()
      logBridge('disabled')
      return getStatus()
    }

    clearReconnectTimer()
    closeSocket()
    connect()
    return getStatus()
  }

  function getConfig() {
    return { ...bridgeState.config }
  }

  function getStatus() {
    return {
      enabled: bridgeState.config.enabled,
      url: bridgeState.config.url,
      connected: bridgeState.connected,
      authenticated: bridgeState.authenticated,
      channelId: bridgeState.config.channelId,
      deviceId: bridgeState.config.deviceId,
      groupId: bridgeState.config.groupId,
      sourceId: bridgeState.config.sourceId,
      sourceMode: bridgeState.config.sourceMode,
      reconnectMs: bridgeState.config.reconnectMs,
      hasToken: Boolean(bridgeState.config.token),
      sentCount: bridgeState.sentCount,
      droppedCount: bridgeState.droppedCount,
      lastSentAt: bridgeState.lastSentAt,
      lastSentTopic: bridgeState.lastSentTopic,
      lastError: bridgeState.lastError,
    }
  }

  if (bridgeState.config.enabled) {
    connect()
  }

  return {
    publish,
    updateConfig,
    getConfig,
    getStatus,
  }
}

const gatewayBridge = createGatewayBridge()

const server = http.createServer((request, response) => {
  if (request.method === 'OPTIONS') {
    response.writeHead(204, {
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Methods': 'GET,POST,OPTIONS',
      'Access-Control-Allow-Headers': 'Content-Type',
    })
    response.end()
    return
  }

  if ((request.url || '').startsWith(PROXY_PATH)) {
    handleProxyRequest(request, response)
    return
  }

  if ((request.url || '').startsWith(BRIDGE_CONFIG_PATH)) {
    if (request.method === 'GET') {
      respondJson(response, 200, {
        config: gatewayBridge.getConfig(),
        status: gatewayBridge.getStatus(),
      })
      return
    }

    if (request.method === 'POST') {
      readJsonBody(request)
        .then((payload) => {
          const status = gatewayBridge.updateConfig(payload)
          respondJson(response, 200, {
            config: gatewayBridge.getConfig(),
            status,
          })
        })
        .catch((error) => {
          respondJson(response, 400, {
            error: error && error.message ? error.message : 'Invalid JSON body',
          })
        })
      return
    }

    respondJson(response, 405, {
      error: 'Method Not Allowed',
    })
    return
  }

  if ((request.url || '').startsWith(BRIDGE_STATUS_PATH)) {
    respondJson(response, 200, gatewayBridge.getStatus())
    return
  }

  serveStatic(request.url || '/', response)
})

const gpsWss = new WebSocketServer({ noServer: true })
const heartRateWss = new WebSocketServer({ noServer: true })
const debugLogWss = new WebSocketServer({ noServer: true })

gpsWss.on('connection', (socket) => {
  socket.on('message', (rawMessage) => {
    const text = String(rawMessage)
    let parsed
    try {
      parsed = JSON.parse(text)
    } catch (_error) {
      return
    }

    if (!isMockGpsPayload(parsed)) {
      return
    }

    const outgoing = JSON.stringify({
      type: 'mock_gps',
      timestamp: Number.isFinite(parsed.timestamp) ? parsed.timestamp : Date.now(),
      channelId: normalizeChannelId(parsed.channelId),
      lat: Number(parsed.lat),
      lon: Number(parsed.lon),
      accuracyMeters: Number.isFinite(parsed.accuracyMeters) ? Number(parsed.accuracyMeters) : 6,
      speedMps: Number.isFinite(parsed.speedMps) ? Number(parsed.speedMps) : 0,
      headingDeg: Number.isFinite(parsed.headingDeg) ? Number(parsed.headingDeg) : 0,
    })
    gatewayBridge.publish(JSON.parse(outgoing))

    gpsWss.clients.forEach((client) => {
      if (client.readyState === client.OPEN) {
        client.send(outgoing)
      }
    })
  })
})

heartRateWss.on('connection', (socket) => {
  socket.on('message', (rawMessage) => {
    const text = String(rawMessage)
    let parsed
    try {
      parsed = JSON.parse(text)
    } catch (_error) {
      return
    }

    if (!isMockHeartRatePayload(parsed)) {
      return
    }

    const outgoing = JSON.stringify({
      type: 'mock_heart_rate',
      timestamp: Number.isFinite(parsed.timestamp) ? parsed.timestamp : Date.now(),
      channelId: normalizeChannelId(parsed.channelId),
      bpm: Math.max(1, Math.round(Number(parsed.bpm))),
    })
    gatewayBridge.publish(JSON.parse(outgoing))

    heartRateWss.clients.forEach((client) => {
      if (client.readyState === client.OPEN) {
        client.send(outgoing)
      }
    })
  })
})

debugLogWss.on('connection', (socket) => {
  socket.on('message', (rawMessage) => {
    const text = String(rawMessage)
    let parsed
    try {
      parsed = JSON.parse(text)
    } catch (_error) {
      return
    }

    if (!isDebugLogPayload(parsed)) {
      return
    }

    const outgoing = JSON.stringify({
      type: 'debug-log',
      timestamp: Number.isFinite(parsed.timestamp) ? parsed.timestamp : Date.now(),
      channelId: normalizeChannelId(parsed.channelId),
      scope: String(parsed.scope || 'app').slice(0, 64),
      level: parsed.level === 'warn' || parsed.level === 'error' ? parsed.level : 'info',
      message: String(parsed.message || '').slice(0, 400),
      ...(parsed.payload && typeof parsed.payload === 'object'
        ? { payload: parsed.payload }
        : {}),
    })

    debugLogWss.clients.forEach((client) => {
      if (client.readyState === client.OPEN) {
        client.send(outgoing)
      }
    })
  })
})

server.on('upgrade', (request, socket, head) => {
  const requestUrl = request.url || ''
  if (requestUrl.startsWith(GPS_WS_PATH)) {
    gpsWss.handleUpgrade(request, socket, head, (ws) => {
      gpsWss.emit('connection', ws, request)
    })
    return
  }

  if (requestUrl.startsWith(HEART_RATE_WS_PATH)) {
    heartRateWss.handleUpgrade(request, socket, head, (ws) => {
      heartRateWss.emit('connection', ws, request)
    })
    return
  }

  if (requestUrl.startsWith(DEBUG_LOG_WS_PATH)) {
    debugLogWss.handleUpgrade(request, socket, head, (ws) => {
      debugLogWss.emit('connection', ws, request)
    })
    return
  }

  if (!requestUrl) {
    socket.destroy()
  }
  socket.destroy()
})

server.listen(PORT, HOST, () => {
  console.log(`Mock GPS simulator running:`)
  console.log(`  UI: http://127.0.0.1:${PORT}/`)
  console.log(`  GPS WS: ws://127.0.0.1:${PORT}${GPS_WS_PATH}`)
  console.log(`  HR WS: ws://127.0.0.1:${PORT}${HEART_RATE_WS_PATH}`)
  console.log(`  Logger WS: ws://127.0.0.1:${PORT}${DEBUG_LOG_WS_PATH}`)
  console.log(`  Proxy: http://127.0.0.1:${PORT}${PROXY_PATH}?url=<remote-url>`)
  console.log(`  Bridge status: http://127.0.0.1:${PORT}${BRIDGE_STATUS_PATH}`)
  console.log(`  Bridge config: http://127.0.0.1:${PORT}${BRIDGE_CONFIG_PATH}`)
  if (INITIAL_BRIDGE_CONFIG.enabled) {
    console.log(`  Gateway bridge: enabled -> ${INITIAL_BRIDGE_CONFIG.url}`)
    console.log(`  Gateway target device: ${INITIAL_BRIDGE_CONFIG.deviceId}`)
  } else {
    console.log(`  Gateway bridge: disabled`)
  }
})
