// Include phoenix_html to handle method=PUT/DELETE in forms and buttons.
import "phoenix_html"
// Establish Phoenix Socket and LiveView configuration.
import {Socket} from "phoenix"
import {LiveSocket} from "phoenix_live_view"
import {hooks as colocatedHooks} from "phoenix-colocated/microwaveprop"
import topbar from "../vendor/topbar"
import "../vendor/canvas-confetti"

import initLiveStash from "../../deps/live_stash/assets/js/live-stash.js"
import {lazyHook, lazyMapHook} from "./lazy_hook"
import {LocateMe, CopyLink} from "./locate_me_hook"
import {RoverSlider} from "./rover_slider_hook"
import {HorizontalWheelScroll} from "./horizontal_wheel_scroll_hook"

// Heavy / page-specific hooks are loaded as their own chunks the first
// time they mount. Leaflet is loaded once via lazyMapHook and shared
// across every map page. Pages that never mount any of these (e.g.
// /submit, /algo, /about) never download these chunks.
const PropagationMap = lazyMapHook(() => import("./propagation_map_hook"), "PropagationMap")
const PathForecast = lazyHook(() => import("./path_forecast_hook"), "PathForecast")
const ContactMap = lazyMapHook(() => import("./contact_map_hook"), "ContactMap")
const ContactsMap = lazyMapHook(() => import("./contacts_map_hook"), "ContactsMap")
const ElevationProfile = lazyHook(() => import("./elevation_profile_hook"), "ElevationProfile")
const WeatherMap = lazyMapHook(() => import("./weather_map_hook"), "WeatherMap")
const RoverMap = lazyMapHook(() => import("./rover_map_hook"), "RoverMap")
const LocationMap = lazyMapHook(() => import("./location_map_hook"), "LocationMap")
const RoverLocationsMap = lazyMapHook(() => import("./rover_locations_map_hook"), "RoverLocationsMap")
const BeaconMap = lazyMapHook(() => import("./beacon_map_hook"), "BeaconMap")
const BeaconsListMap = lazyMapHook(() => import("./beacons_list_map_hook"), "BeaconsListMap")
const EmeGlobe = lazyHook(() => import("./eme_globe_hook"), "EmeGlobe")

interface UtcClockHook extends ViewHook {
  timer: ReturnType<typeof setInterval>
  tick(): void
}

const UtcClock = {
  mounted(this: UtcClockHook) {
    this.tick()
    this.timer = setInterval(() => this.tick(), 10_000)
  },
  tick(this: UtcClockHook) {
    const now = new Date()
    const h = now.getUTCHours().toString().padStart(2, "0")
    const m = now.getUTCMinutes().toString().padStart(2, "0")
    this.el.textContent = `${h}:${m} UTC`
  },
  destroyed(this: UtcClockHook) {
    clearInterval(this.timer)
  }
}

interface CommaNumberHook extends ViewHook {
  format(): void
}

// Realistic-look recipe from https://confetti.js.org (the "realistic" preset).
// Fires exactly once when the hook element mounts — we attach the hook to
// the import-complete banner, which only renders when run.status == "complete".
const ImportConfetti = {
  mounted(this: ViewHook) {
    const fire = window.confetti
    if (typeof fire !== "function") return
    const count = 200
    const defaults = { origin: { y: 0.7 } }
    const burst = (particleRatio: number, opts: ConfettiOptions) =>
      fire({ ...defaults, ...opts, particleCount: Math.floor(count * particleRatio) })
    burst(0.25, { spread: 26, startVelocity: 55 })
    burst(0.2, { spread: 60 })
    burst(0.35, { spread: 100, decay: 0.91, scalar: 0.8 })
    burst(0.1, { spread: 120, startVelocity: 25, decay: 0.92, scalar: 1.2 })
    burst(0.1, { spread: 120, startVelocity: 45 })
  }
}

const CommaNumber = {
  mounted(this: CommaNumberHook) {
    this.el.addEventListener("input", () => this.format())
    this.format()
  },
  updated(this: CommaNumberHook) { this.format() },
  format(this: CommaNumberHook) {
    const input = (this.el.querySelector("input") || this.el) as HTMLInputElement
    const raw = input.value.replace(/,/g, "")
    if (raw === "" || raw === "-") return
    const cursor = input.selectionStart ?? 0
    const commasBefore = (input.value.slice(0, cursor).match(/,/g) || []).length
    const parts = raw.split(".")
    parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ",")
    const formatted = parts.join(".")
    if (formatted !== input.value) {
      input.value = formatted
      const commasAfter = (formatted.slice(0, cursor).match(/,/g) || []).length
      const newCursor = cursor + (commasAfter - commasBefore)
      input.setSelectionRange(newCursor, newCursor)
    }
  }
}

// Skew-T crosshair: tracks mouse Y, inverts pressure_y to find the
// pressure level under the cursor, interpolates T/Td/height from the
// profile array (passed in via data-profile JSON) and updates the
// crosshair line + readout that SkewtSvg emits with display:none.
interface SkewtHook extends ViewHook {
  svg: SVGSVGElement | null
  cursor: SVGElement | null
  line: SVGLineElement | null
  dotT: SVGCircleElement | null
  dotD: SVGCircleElement | null
  bg: SVGRectElement | null
  text: SVGTextElement | null
  lines: (SVGTSpanElement | null)[]
  geom: SkewtGeom
  normalized: SkewtLevel[]
  onMove(e: MouseEvent): void
  onLeave(): void
  pressureAtY(y: number): number
  temperatureX(t: number, y: number): number
  interp(field: "tmpc" | "dwpc" | "hght", p: number): number | null
  clientToViewBox(e: MouseEvent): {x: number, y: number} | null
  handleMove(e: MouseEvent): void
}

interface SkewtGeom {
  width: number; height: number
  left: number; right: number; top: number; bottom: number
  p_bottom: number; p_top: number
  t_min: number; t_max: number
  skew: number
}

interface SkewtLevel {
  pres: number
  hght: number
  tmpc: number | null
  dwpc: number | null
}

const Skewt = {
  mounted(this: SkewtHook) {
    this.svg = this.el.querySelector("svg")
    this.cursor = this.el.querySelector("[data-cursor]")
    this.line = this.el.querySelector(".skewt-cursor-line")
    this.dotT = this.el.querySelector("[data-cursor-dot-t]")
    this.dotD = this.el.querySelector("[data-cursor-dot-d]")
    this.bg = this.el.querySelector("[data-cursor-readout-bg]")
    this.text = this.el.querySelector("[data-cursor-readout]")
    this.lines = [
      this.el.querySelector("[data-cursor-line-1]"),
      this.el.querySelector("[data-cursor-line-2]"),
      this.el.querySelector("[data-cursor-line-3]"),
      this.el.querySelector("[data-cursor-line-4]"),
    ]

    let rawProfile: any[] = []
    try { rawProfile = JSON.parse(this.el.dataset.profile || "[]") } catch { rawProfile = [] }
    try { this.geom = JSON.parse(this.el.dataset.geometry || "{}") } catch { this.geom = {} as SkewtGeom }

    this.normalized = []
    for (const lvl of rawProfile) {
      const pres = +(lvl.pres ?? lvl.pres_mb)
      const hght = +(lvl.hght ?? lvl.hght_m)
      const tmpc = lvl.tmpc, dwpc = lvl.dwpc
      if (Number.isFinite(pres) && Number.isFinite(hght)) {
        this.normalized.push({
          pres, hght,
          tmpc: Number.isFinite(tmpc) ? +tmpc : null,
          dwpc: Number.isFinite(dwpc) ? +dwpc : null,
        })
      }
    }
    this.normalized.sort((a, b) => b.pres - a.pres)

    this.onMove = (e: MouseEvent) => this.handleMove(e)
    this.onLeave = () => { if (this.cursor) (this.cursor as SVGElement).style.display = "none" }
    if (this.svg) {
      this.svg.addEventListener("mousemove", this.onMove)
      this.svg.addEventListener("mouseleave", this.onLeave)
    }
  },

  destroyed(this: SkewtHook) {
    if (this.svg) {
      this.svg.removeEventListener("mousemove", this.onMove)
      this.svg.removeEventListener("mouseleave", this.onLeave)
    }
  },

  pressureAtY(this: SkewtHook, y: number) {
    const g = this.geom
    const lnB = Math.log(g.p_bottom)
    const lnT = Math.log(g.p_top)
    const frac = (g.bottom - y) / (g.bottom - g.top)
    return Math.exp(lnB - frac * (lnB - lnT))
  },

  temperatureX(this: SkewtHook, t: number, y: number) {
    const g = this.geom
    const plotW = g.right - g.left
    const baseX = g.left + ((t - g.t_min) / (g.t_max - g.t_min)) * plotW
    return baseX + g.skew * (g.bottom - y)
  },

  interp(this: SkewtHook, field: "tmpc" | "dwpc" | "hght", p: number): number | null {
    const prof = this.normalized
    if (prof.length === 0) return null
    if (p >= prof[0].pres) return prof[0][field] as number | null
    if (p <= prof[prof.length - 1].pres) return prof[prof.length - 1][field] as number | null
    for (let i = 0; i < prof.length - 1; i++) {
      const a = prof[i], b = prof[i + 1]
      if (p <= a.pres && p >= b.pres) {
        const av = a[field] as number | null, bv = b[field] as number | null
        if (av == null || bv == null) return av ?? bv
        const f = (a.pres - p) / (a.pres - b.pres)
        return av + f * (bv - av)
      }
    }
    return null
  },

  clientToViewBox(this: SkewtHook, e: MouseEvent) {
    if (!this.svg) return null
    const ctm = this.svg.getScreenCTM()
    if (!ctm) return null
    const pt = this.svg.createSVGPoint()
    pt.x = e.clientX
    pt.y = e.clientY
    const local = pt.matrixTransform(ctm.inverse())
    return {x: local.x, y: local.y}
  },

  handleMove(this: SkewtHook, e: MouseEvent) {
    if (this.normalized.length === 0) return
    const v = this.clientToViewBox(e)
    if (!v) return
    const g = this.geom
    if (v.y < g.top || v.y > g.bottom) {
      if (this.cursor) (this.cursor as SVGElement).style.display = "none"
      return
    }

    const p = this.pressureAtY(v.y)
    const tC = this.interp("tmpc", p)
    const tdC = this.interp("dwpc", p)
    const hgtM = this.interp("hght", p)

    if (this.line) { this.line.setAttribute("y1", String(v.y)); this.line.setAttribute("y2", String(v.y)) }

    if (tC != null && this.dotT) {
      this.dotT.setAttribute("cx", String(this.temperatureX(tC, v.y)))
      this.dotT.setAttribute("cy", String(v.y))
      this.dotT.style.display = ""
    } else if (this.dotT) {
      this.dotT.style.display = "none"
    }
    if (tdC != null && this.dotD) {
      this.dotD.setAttribute("cx", String(this.temperatureX(tdC, v.y)))
      this.dotD.setAttribute("cy", String(v.y))
      this.dotD.style.display = ""
    } else if (this.dotD) {
      this.dotD.style.display = "none"
    }

    const boxX = g.left + 6
    const boxY = g.top + 6
    if (this.bg) { this.bg.setAttribute("x", String(boxX)); this.bg.setAttribute("y", String(boxY)) }
    if (this.text) { this.text.setAttribute("x", String(boxX + 6)); this.text.setAttribute("y", String(boxY + 4)) }
    this.lines.forEach(el => el?.setAttribute("x", String(boxX + 6)))

    const ftStr = (hgtM != null && Number.isFinite(hgtM)) ? Math.round(hgtM * 3.28084).toLocaleString() : "—"
    const mStr = (hgtM != null && Number.isFinite(hgtM)) ? Math.round(hgtM).toLocaleString() : "—"
    const fmt = (val: number | null) => val == null || !Number.isFinite(val) ? "—" : `${val.toFixed(1)}°C`
    if (this.lines[0]) this.lines[0]!.textContent = `${p.toFixed(1)} mb`
    if (this.lines[1]) this.lines[1]!.textContent = `${mStr} m / ${ftStr} ft`
    if (this.lines[2]) this.lines[2]!.textContent = `T  ${fmt(tC)}`
    if (this.lines[3]) this.lines[3]!.textContent = `Td ${fmt(tdC)}`

    if (this.cursor) (this.cursor as SVGElement).style.display = ""
  }
} as Partial<SkewtHook>

const csrfToken = document.querySelector("meta[name='csrf-token']")!.getAttribute("content")
const liveSocket = new LiveSocket("/live", Socket, {
  longPollFallbackMs: 2500,
  params: initLiveStash({_csrf_token: csrfToken}),
  hooks: {...colocatedHooks, PropagationMap, PathForecast, UtcClock, ContactMap, ContactsMap, ElevationProfile, WeatherMap, LocateMe, CopyLink, RoverMap, LocationMap, RoverLocationsMap, RoverSlider, HorizontalWheelScroll, BeaconMap, BeaconsListMap, CommaNumber, ImportConfetti, EmeGlobe, Skewt},
})

// live_table rows are dead on their own — wire up whole-row clicks to
// trigger the first <a href> inside that row (typically the Actions "View"
// link). Clicks on interactive elements are ignored so inner links/buttons
// still win.
document.addEventListener("click", (e: MouseEvent) => {
  const target = e.target as HTMLElement | null
  if (!target) return
  if (target.closest("a, button, input, select, label, [role='option'], [role='menuitem']")) return
  const tr = target.closest("#live-table tbody tr[id]") as HTMLTableRowElement | null
  if (!tr || tr.id === "empty-placeholder") return
  const link = tr.querySelector("a[href]") as HTMLAnchorElement | null
  if (link) link.click()
})

// Show progress bar on live navigation and form submits
topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"})
window.addEventListener("phx:page-loading-start", _info => topbar.show(300))
window.addEventListener("phx:page-loading-stop", _info => topbar.hide())

// Delay disconnect alerts by 3 seconds so brief hiccups don't flash errors
let disconnectTimer: ReturnType<typeof setTimeout> | null = null
const DISCONNECT_DELAY_MS = 3000

function showDisconnectAlert(id: string) {
  const el = document.getElementById(id)
  if (el) el.removeAttribute("hidden")
}

function hideDisconnectAlert(id: string) {
  const el = document.getElementById(id)
  if (el) el.setAttribute("hidden", "")
}

new MutationObserver(() => {
  const root = document.querySelector("[data-phx-main]")
  if (!root) return

  const hasError = root.classList.contains("phx-client-error") ||
                   root.classList.contains("phx-server-error")

  if (hasError && !disconnectTimer) {
    disconnectTimer = setTimeout(() => {
      const r = document.querySelector("[data-phx-main]")
      if (r?.classList.contains("phx-client-error")) showDisconnectAlert("client-error")
      if (r?.classList.contains("phx-server-error")) showDisconnectAlert("server-error")
    }, DISCONNECT_DELAY_MS)
  } else if (!hasError && disconnectTimer) {
    clearTimeout(disconnectTimer)
    disconnectTimer = null
    hideDisconnectAlert("client-error")
    hideDisconnectAlert("server-error")
  }
}).observe(document.body, {subtree: true, attributes: true, attributeFilter: ["class"]})

// connect if there are any LiveViews on the page
liveSocket.connect()

// expose liveSocket on window for web console debug logs and latency simulation:
// >> liveSocket.enableDebug()
// >> liveSocket.enableLatencySim(1000)  // enabled for duration of browser session
// >> liveSocket.disableLatencySim()
window.liveSocket = liveSocket

// The lines below enable quality of life phoenix_live_reload
// development features:
//
//     1. stream server logs to the browser console
//     2. click on elements to jump to their definitions in your code editor
//
if (process.env.NODE_ENV === "development") {
  // Phoenix.LiveReloader attaches a small API surface to its detached
  // `reloader` instance. Type only what we actually touch instead of `any`.
  type Reloader = {
    enableServerLogs: () => void
    openEditorAtCaller: (target: EventTarget | null) => void
    openEditorAtDef: (target: EventTarget | null) => void
  }
  window.addEventListener("phx:live_reload:attached", ((event: CustomEvent<Reloader>) => {
    const reloader = event.detail
    // Enable server log streaming to client.
    // Disable with reloader.disableServerLogs()
    reloader.enableServerLogs()

    // Open configured PLUG_EDITOR at file:line of the clicked element's HEEx component
    //
    //   * click with "c" key pressed to open at caller location
    //   * click with "d" key pressed to open at function component definition location
    let keyDown: string | null
    window.addEventListener("keydown", (e: KeyboardEvent) => keyDown = e.key)
    window.addEventListener("keyup", (_e: KeyboardEvent) => keyDown = null)
    window.addEventListener("click", (e: MouseEvent) => {
      if(keyDown === "c"){
        e.preventDefault()
        e.stopImmediatePropagation()
        reloader.openEditorAtCaller(e.target)
      } else if(keyDown === "d"){
        e.preventDefault()
        e.stopImmediatePropagation()
        reloader.openEditorAtDef(e.target)
      }
    }, true)

    window.liveReloader = reloader
  }) as EventListener)
}
