import { getCurrentUser } from '@/lib/auth/current-user'
import { env } from '@/lib/env'
import { demoBaseTimestamp } from '@/lib/demo/config'
import { getLiveGrid, getScenarioGrid, getSceneAlerts } from '@/lib/services/live-grid-service'

export const dynamic = 'force-dynamic'
export const runtime = 'nodejs'

/**
 * The Live 3D Grid stream (§7, §85).
 *
 * Server-Sent Events, the same transport the grid stream already uses: traffic is strictly
 * server→client, it survives proxies that mishandle upgrades, and the browser reconnects
 * on its own. A WebSocket would add a second protocol and a second failure mode for
 * nothing — there are no client→server messages on this channel.
 *
 * **The payload carries no geometry.** Positions do not change every two seconds, and
 * re-sending a scene's worth of coordinates on every tick is the surest way to make a
 * live view feel slow. The client loads topology once from `/api/live-grid/topology` and
 * this stream sends only what moves — which is also what lets the renderer update the
 * affected meshes rather than rebuilding the scene (§85).
 *
 * The tick is the platform's configured telemetry interval, floored at one second. This
 * is not polling: one connection, server-paced, cleaned up when the tab closes.
 */
export async function GET(request: Request) {
  const user = await getCurrentUser()
  if (!user || !user.permissions.has('twin.read')) {
    return new Response(
      JSON.stringify({
        ok: false,
        error: { code: 'PERMISSION_DENIED', message: 'Not permitted.', messageAr: 'غير مصرح.' },
      }),
      { status: 403, headers: { 'Content-Type': 'application/json' } },
    )
  }

  const url = new URL(request.url)
  const twinCode = url.searchParams.get('twin') ?? undefined
  const scenario = url.searchParams.get('scenario')
  const offsetMin = Number(url.searchParams.get('offset')) || 0
  const demoClock = url.searchParams.get('clock') === 'demo'

  const encoder = new TextEncoder()
  const tickMs = Math.max(1_000, env.telemetryTickMs)

  const stream = new ReadableStream<Uint8Array>({
    async start(controller) {
      let closed = false
      let alertTick = 0

      const send = (event: string, data: unknown) => {
        if (closed) return
        try {
          controller.enqueue(encoder.encode(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`))
        } catch {
          closed = true
        }
      }

      const push = async () => {
        if (closed) return
        try {
          // The demo clock advances with wall time from its pinned base, so a
          // demonstration still moves — it simply moves through the afternoon it is
          // meant to show rather than through the small hours.
          const ts = demoClock ? demoBaseTimestamp() : undefined
          const payload =
            scenario && scenario !== 'normal'
              ? await getScenarioGrid(scenario, { twinCode, offsetMin, ts })
              : await getLiveGrid({ twinCode, offsetMin, ts })

          if (!payload) {
            send('stream-error', { message: 'twin_not_found' })
            return
          }

          // Alerts change on the scale of minutes, not seconds, so they ride every tenth
          // tick rather than every one.
          const withAlerts = alertTick % 10 === 0
          alertTick += 1

          send('grid', {
            mode: payload.mode,
            kind: payload.kind,
            ts: payload.ts,
            offsetMin: payload.offsetMin,
            scenario: payload.scenario,
            // Only the fields the scene and the labels actually read.
            assets: payload.assets.map((asset) => ({
              code: asset.code,
              loadPct: asset.loadPct,
              loadMw: asset.loadMw,
              voltageKv: asset.voltageKv,
              voltageDeviationPct: asset.voltageDeviationPct,
              currentA: asset.currentA,
              powerFactor: asset.powerFactor,
              activePowerMw: asset.activePowerMw,
              reactivePowerMvar: asset.reactivePowerMvar,
              tempC: asset.tempC,
              thermalStress: asset.thermalStress,
              healthScore: asset.healthScore,
              riskScore: asset.riskScore,
              state: asset.state,
              isOutage: asset.isOutage,
              socPct: asset.socPct,
              role: asset.role,
            })),
            edges: payload.flow.edges.map((edge) => ({
              from: edge.from,
              to: edge.to,
              direction: edge.direction,
              activePowerMw: edge.activePowerMw,
              currentA: edge.currentA,
              utilisationPct: edge.utilisationPct,
              intensity: edge.intensity,
              state: edge.state,
            })),
            kpis: payload.kpis,
            connection: payload.connection,
            fidelity: payload.fidelity,
            ...(withAlerts
              ? { alerts: await getSceneAlerts(payload.assets.map((asset) => asset.code)) }
              : {}),
          })
        } catch (error) {
          console.error('[live-grid stream] tick failed', error)
          send('stream-error', { message: 'tick_failed' })
        }
      }

      controller.enqueue(encoder.encode(`retry: ${tickMs * 2}\n\n`))
      await push()

      const interval = setInterval(push, tickMs)

      const stop = () => {
        if (closed) return
        closed = true
        clearInterval(interval)
        try {
          controller.close()
        } catch {
          /* already closed */
        }
      }

      request.signal.addEventListener('abort', stop)
    },

    cancel() {
      // Reached when the consumer disconnects; the abort listener does the cleanup.
    },
  })

  return new Response(stream, {
    headers: {
      'Content-Type': 'text/event-stream; charset=utf-8',
      'Cache-Control': 'no-cache, no-transform',
      Connection: 'keep-alive',
      'X-Accel-Buffering': 'no',
    },
  })
}
