import type { Tunnel } from '@because/sneaker'
import { connect } from '@because/sneaker'
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'
import { join } from 'path'
import { getApp, TOES_DIR, update } from '$apps'
import { hostLog } from './tui'

const SNEAKER_URL = process.env.SNEAKER_URL ?? 'https://toes.space'

type TunnelConfig = Record<string, { subdomain?: string }>

const RECONNECT_DELAYS = [5000, 10000, 20000, 30000, 60000]
const MAX_RECONNECT_ATTEMPTS = 10

const _closing = new Set<string>()
const _reconnectAttempts = new Map<string, number>()
const _reconnectTimers = new Map<string, Timer>()
const _tunnels = new Map<string, Tunnel>()
const _tunnelPorts = new Map<string, number>()

const configPath = () =>
  join(TOES_DIR, 'tunnels.json')

const loadConfig = (): TunnelConfig => {
  const path = configPath()
  if (!existsSync(path)) return {}
  try {
    return JSON.parse(readFileSync(path, 'utf-8'))
  } catch {
    return {}
  }
}

const saveConfig = (config: TunnelConfig) => {
  const dir = TOES_DIR
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
  writeFileSync(configPath(), JSON.stringify(config, null, 2) + '\n')
}

const toWsUrl = (url: string): string =>
  url.replace(/^http:/, 'ws:').replace(/^https:/, 'wss:')

function buildTunnelUrl(subdomain: string): string {
  const parsed = new URL(SNEAKER_URL!)
  const port = parsed.port && parsed.port !== '80' && parsed.port !== '443'
    ? `:${parsed.port}`
    : ''
  return `${parsed.protocol}//${subdomain}.${parsed.hostname}${port}`
}

export const isTunnelsAvailable = (): boolean =>
  !!SNEAKER_URL

export function closeAllTunnels() {
  for (const [, timer] of _reconnectTimers) {
    clearTimeout(timer)
  }
  _reconnectTimers.clear()
  _reconnectAttempts.clear()

  for (const [name, tunnel] of _tunnels) {
    _closing.add(name)
    tunnel.close()
  }
  _tunnels.clear()
  _tunnelPorts.clear()
}

export function closeTunnel(appName: string) {
  cancelReconnect(appName)

  const tunnel = _tunnels.get(appName)
  if (!tunnel) return

  _closing.add(appName)
  tunnel.close()
  _tunnels.delete(appName)
  _tunnelPorts.delete(appName)

  const app = getApp(appName)
  if (app) {
    app.tunnelEnabled = false
    app.tunnelUrl = undefined
    update()
  }
}

export function unshareApp(appName: string) {
  closeTunnel(appName)

  const config = loadConfig()
  delete config[appName]
  saveConfig(config)

  const app = getApp(appName)
  if (app) {
    app.tunnelEnabled = false
    update()
  }
}

export function shareApp(appName: string, port: number) {
  if (!SNEAKER_URL) return

  const app = getApp(appName)
  if (app) {
    app.tunnelEnabled = true
    update()
  }

  // Save to config (even if port is 0, we want it enabled for when the app starts)
  const config = loadConfig()
  const existing = config[appName]
  config[appName] = existing ?? {}
  saveConfig(config)

  if (port > 0) {
    openTunnel(appName, port, config[appName]?.subdomain)
  }
}

export function openTunnelIfEnabled(appName: string, port: number) {
  if (!SNEAKER_URL) return

  const config = loadConfig()
  if (!config[appName]) return

  const app = getApp(appName)
  if (app) {
    app.tunnelEnabled = true
    update()
  }

  // Skip if tunnel is already open on the same port
  if (_tunnels.has(appName) && _tunnelPorts.get(appName) === port) return

  openTunnel(appName, port, config[appName]?.subdomain)
}

export function renameTunnelConfig(oldName: string, newName: string) {
  const config = loadConfig()
  if (!config[oldName]) return

  config[newName] = config[oldName]!
  delete config[oldName]
  saveConfig(config)
}

function cancelReconnect(appName: string, resetAttempts = true) {
  const timer = _reconnectTimers.get(appName)
  if (timer) {
    clearTimeout(timer)
    _reconnectTimers.delete(appName)
  }
  if (resetAttempts) _reconnectAttempts.delete(appName)
}

function openTunnel(appName: string, port: number, subdomain?: string, isReconnect = false) {
  // Cancel any pending reconnect timer to prevent duplicate loops
  // but preserve attempts counter during reconnection so backoff works
  cancelReconnect(appName, !isReconnect)

  // Close existing tunnel if any
  const existing = _tunnels.get(appName)
  if (existing) {
    _closing.add(appName)
    existing.close()
    _tunnels.delete(appName)
  }

  const wsUrl = toWsUrl(SNEAKER_URL!)

  const tunnel = connect({
    server: wsUrl,
    app: appName,
    target: `http://localhost:${port}`,
    subdomain,
    reconnect: false,

    onRequest(req) {
      const app = getApp(appName)
      if (app?.tunnelUrl) req.headers['x-app-url'] = app.tunnelUrl
      // Static apps are served by the main server via subdomain routing,
      // so set the Host header so extractSubdomain() can identify the app
      if (app?.static) req.headers['host'] = `${appName}.localhost`
    },

    onOpen(assignedSubdomain) {
      hostLog(`Tunnel open: ${appName} -> ${assignedSubdomain}`)

      // Reset reconnect attempts on successful connection
      _reconnectAttempts.delete(appName)

      // Save subdomain for reconnection
      const config = loadConfig()
      if (config[appName]) {
        config[appName]!.subdomain = assignedSubdomain
        saveConfig(config)
      }

      const app = getApp(appName)
      if (app) {
        app.tunnelUrl = buildTunnelUrl(assignedSubdomain)
        update()
      }
    },

    onClose() {
      _tunnels.delete(appName)
      _tunnelPorts.delete(appName)

      // Intentional close (unshareApp, closeAllTunnels, etc.) — don't reconnect
      if (_closing.delete(appName)) {
        hostLog(`Tunnel closed: ${appName}`)
        return
      }

      // Check retry limit
      const attempts = _reconnectAttempts.get(appName) ?? 0
      if (attempts >= MAX_RECONNECT_ATTEMPTS) {
        hostLog(`Tunnel gave up: ${appName} (${attempts} failed attempts)`)
        _reconnectAttempts.delete(appName)
        return
      }

      const delayIndex = Math.min(attempts, RECONNECT_DELAYS.length - 1)
      const delay = RECONNECT_DELAYS[delayIndex]!
      _reconnectAttempts.set(appName, attempts + 1)

      hostLog(`Tunnel dropped: ${appName}, reconnecting in ${delay / 1000}s (attempt ${attempts + 1}/${MAX_RECONNECT_ATTEMPTS})...`)
      const app = getApp(appName)
      if (app) {
        app.tunnelUrl = undefined
        update()
      }

      const timer = setTimeout(() => {
        _reconnectTimers.delete(appName)

        // Only reconnect if still enabled in config
        const config = loadConfig()
        if (!config[appName]) return
        hostLog(`Tunnel reconnecting: ${appName}`)
        openTunnel(appName, port, config[appName]?.subdomain, true)
      }, delay)

      _reconnectTimers.set(appName, timer)
    },

    onError(error) {
      hostLog(`Tunnel error (${appName}): ${error.message}`)
    },
  })

  _tunnels.set(appName, tunnel)
  _tunnelPorts.set(appName, port)
}
