---
const weatherBase = (import.meta.env.PUBLIC_WEATHER_BASE ?? 'https://wx.swansea-airport.wales').replace(/\/$/, '');
const mqttHost = import.meta.env.PUBLIC_WEATHER_MQTT_HOST ?? `${weatherBase}/mqtt`;
const archiveEndpoint = `${weatherBase}/agcs/archive.php`;
const pressureEndpoint = `${weatherBase}/agcs/wlproxy.php?api=current/195562`;
---

<section class="weather-shell" aria-labelledby="weather-heading">
  <div class="weather-head">
    <div>
      <p class="eyebrow">Live airfield weather</p>
      <h1 id="weather-heading" class="section-title">Swansea EGFH conditions</h1>
      <p class="section-copy">
        Live readings from the airport weather station. Values update automatically where the
        upstream feed is available. This page is for situational awareness only; always use
        official pre-flight sources, NOTAMs, and radio updates for flight safety decisions.
      </p>
    </div>
    <div class="weather-status" aria-live="polite">
      <span class="status-dot waiting" id="connection-dot"></span>
      <span id="connection-status">Connecting</span>
    </div>
  </div>

  <div class="weather-dashboard">
    <div class="weather-main-column">
      <article class="weather-card weather-wind-card">
        <div class="weather-wind-primary">
          <div class="weather-metric-block">
            <p class="weather-label">Surface wind</p>
            <p class="weather-value" id="avg-wind">XXX/XX</p>
            <p class="weather-meta" id="avg-wind-meta">Waiting for 2 minute average</p>
          </div>
          <div class="weather-metric-block">
            <p class="weather-label">Instant wind</p>
            <p class="weather-value" id="instant-wind">XXX/XX</p>
            <p class="weather-meta" id="instant-wind-meta">Waiting for live wind</p>
          </div>
        </div>
      </article>

      <div class="weather-grid">
        <article class="weather-card weather-pressure-card">
          <div class="weather-pressure-grid">
            <div class="weather-metric-block">
              <p class="weather-pressure-label">QNH</p>
              <p class="weather-pressure-value" id="qnh">XXX</p>
            </div>
            <div class="weather-metric-block">
              <p class="weather-pressure-label">QFE</p>
              <p class="weather-pressure-value" id="qfe">XXX</p>
            </div>
          </div>
          <p class="weather-meta" id="pressure-meta">Waiting for pressure</p>
        </article>

        <article class="weather-card">
          <p class="weather-label">2 minute gust</p>
          <p class="weather-value" id="gust">XXX/XX</p>
        </article>

        <article class="weather-card">
          <p class="weather-label">OAT</p>
          <p class="weather-value" id="oat">XXX</p>
        </article>
      </div>
      <p class="weather-feed-note" id="feed-note">
        Live weather is initialising.
      </p>
    </div>

    <div class="weather-side-column">
      <article class="weather-card weather-compass-card">
        <div class="weather-card-heading">
          <p class="weather-label">Wind over runway layout</p>
          <span class="pill" id="wind-source">Awaiting feed</span>
        </div>
        <canvas id="weather-compass" width="720" height="450" aria-label="Runway layout with live wind direction"></canvas>
      </article>
    </div>
  </div>
</section>

<script define:vars={{ mqttHost, archiveEndpoint, pressureEndpoint }}>
  (() => {
    const MQTT_SCRIPT = 'https://cdnjs.cloudflare.com/ajax/libs/paho-mqtt/1.0.1/mqttws31.js';
    const MPS_TO_KT = 0.868976;
    const F_TO_C = (fahrenheit) => (fahrenheit - 32) * 5 / 9;
    const STALE = {
      instantWind: 10000,
      avgWind: 150000,
      pressure: 1800000,
      oat: 300000,
    };

    const state = {
      connected: false,
      avgWind: null,
      instantWind: null,
      gust: null,
      oat: null,
      qfe: null,
      qnh: null,
      lastAvgWind: 0,
      lastInstantWind: 0,
      lastOat: 0,
      lastPressure: 0,
      windDirection: null,
    };

    let mqttClient;
    let ctx;

    const get = (id) => document.getElementById(id);

    function loadScript(src) {
      return new Promise((resolve, reject) => {
        if (window.Paho?.MQTT) {
          resolve();
          return;
        }

        const existing = document.querySelector(`script[src="${src}"]`);
        if (existing) {
          existing.addEventListener('load', resolve, { once: true });
          existing.addEventListener('error', reject, { once: true });
          return;
        }

        const script = document.createElement('script');
        script.src = src;
        script.async = true;
        script.onload = resolve;
        script.onerror = reject;
        document.head.append(script);
      });
    }

    function isValidWind(speed, direction, gustSpeed, gustDirection) {
      const validSpeed = Number.isFinite(speed) && speed >= 0 && speed <= 200;
      const validDirection = Number.isFinite(direction) && direction >= 0 && direction <= 360;
      const validGustSpeed = gustSpeed === undefined || (Number.isFinite(gustSpeed) && gustSpeed >= 0 && gustSpeed <= 200);
      const validGustDirection = gustDirection === undefined || (Number.isFinite(gustDirection) && gustDirection >= 0 && gustDirection <= 360);
      return validSpeed && validDirection && validGustSpeed && validGustDirection;
    }

    function roundDirection(direction) {
      let rounded = Math.round(direction / 10) * 10;
      if (rounded === 0) rounded = 360;
      return String(rounded).padStart(3, '0');
    }

    function formatWind(reading) {
      if (!reading) return 'XXX/XX';
      if (reading.speed < 2) return 'CALM';
      return `${roundDirection(reading.direction)}/${Math.round(reading.speed)}`;
    }

    function setConnection(status, className) {
      get('connection-status').textContent = status;
      get('connection-dot').className = `status-dot ${className}`;
    }

    function setText(id, value) {
      const element = get(id);
      if (element.textContent === value) return;
      element.textContent = value;
      element.classList.remove('weather-flash');
      window.requestAnimationFrame(() => element.classList.add('weather-flash'));
    }

    function setFeedNote(message) {
      get('feed-note').textContent = message;
    }

    function saveSnapshot() {
      localStorage.setItem('swansea-weather', JSON.stringify({ ...state, savedAt: Date.now() }));
    }

    function loadSnapshot() {
      try {
        const raw = localStorage.getItem('swansea-weather');
        if (!raw) return;
        const cached = JSON.parse(raw);
        if (Date.now() - cached.savedAt > 3600000) return;
        Object.assign(state, cached);
      } catch (error) {
        console.warn('Could not load cached weather', error);
      }
    }

    function render() {
      const now = Date.now();
      const avgFresh = state.lastAvgWind && now - state.lastAvgWind <= STALE.avgWind;
      const instantFresh = state.lastInstantWind && now - state.lastInstantWind <= STALE.instantWind;
      const oatFresh = state.lastOat && now - state.lastOat <= STALE.oat;
      const pressureFresh = state.lastPressure && now - state.lastPressure <= STALE.pressure;

      setText('avg-wind', avgFresh ? formatWind(state.avgWind) : 'XXX/XX');
      setText('instant-wind', instantFresh ? formatWind(state.instantWind) : 'XXX/XX');
      setText('gust', avgFresh ? formatWind(state.gust) : 'XXX/XX');
      setText('oat', oatFresh && state.oat !== null ? `${Math.round(state.oat)} \u00b0C` : 'XXX');
      setText('qfe', pressureFresh && state.qfe !== null ? String(state.qfe) : 'XXX');
      setText('qnh', pressureFresh && state.qnh !== null ? String(state.qnh) : 'XXX');

      get('avg-wind-meta').textContent = avgFresh ? '2 minute average' : 'Average wind stale';
      get('instant-wind-meta').textContent = instantFresh ? 'Latest live sample' : 'Instant wind stale';
      get('pressure-meta').textContent = pressureFresh ? 'Pressure current' : 'Pressure unavailable';
      get('wind-source').textContent = avgFresh ? 'Live' : 'No current wind';

      if (state.connected && avgFresh && instantFresh) {
        setConnection('Live', 'ok');
      } else if (state.connected) {
        setConnection('Partial feed', 'waiting');
      }

      drawCompass(avgFresh ? state.windDirection : null);
    }

    function applyLoop(loop) {
      const speed = Number(loop.windSpeed_knot);
      const direction = Number(loop.windDir);
      const gustSpeed = Number(loop.windGust_knot);
      const gustDirection = Number(loop.windGustDir);

      if (loop.interval_minute) {
        if (!isValidWind(speed, direction, gustSpeed, gustDirection)) return;
        state.avgWind = { speed, direction };
        state.gust = { speed: gustSpeed, direction: gustDirection };
        state.windDirection = direction;
        state.lastAvgWind = Date.now();
      } else {
        if (isValidWind(speed, direction)) {
          state.instantWind = { speed, direction };
          state.lastInstantWind = Date.now();
        }
        const oat = Number(loop.outTemp_C);
        if (Number.isFinite(oat)) {
          state.oat = oat;
          state.lastOat = Date.now();
        }
      }

      saveSnapshot();
      render();
    }

    async function fetchArchive() {
      try {
        const response = await fetch(archiveEndpoint, { cache: 'no-store' });
        if (!response.ok) throw new Error(`Archive responded ${response.status}`);
        const archive = await response.json();
        const timestamp = Number(archive.dateTime) * 1000;
        if (!timestamp || Date.now() - timestamp > STALE.oat) return;

        const avgSpeed = Number(archive.windSpeed) * MPS_TO_KT;
        const avgDirection = Number(archive.windDir);
        const gustSpeed = Number(archive.windGust) * MPS_TO_KT;
        const gustDirection = Number(archive.windGustDir);
        const oat = F_TO_C(Number(archive.outTemp));

        if (!isValidWind(avgSpeed, avgDirection, gustSpeed, gustDirection) || !Number.isFinite(oat)) return;
        state.avgWind = { speed: avgSpeed, direction: avgDirection };
        state.gust = { speed: gustSpeed, direction: gustDirection };
        state.oat = oat;
        state.windDirection = avgDirection;
        state.lastAvgWind = timestamp;
        state.lastOat = timestamp;
        render();
        saveSnapshot();
      } catch (error) {
        console.warn('Archive weather unavailable', error);
        setFeedNote('Live MQTT can still update wind and temperature. Archive JSON needs CORS enabled on the weather host for browser access.');
      }
    }

    async function fetchPressure() {
      try {
        const response = await fetch(pressureEndpoint, { cache: 'no-store' });
        if (!response.ok) throw new Error(`Pressure responded ${response.status}`);
        const data = await response.json();
        const pressure = data?.contents?.sensors?.[2]?.data?.[0];
        const timestamp = Number(pressure?.ts) * 1000;
        const barAbsolute = Number(pressure?.bar_absolute);
        if (!timestamp || Date.now() - timestamp > STALE.pressure || !Number.isFinite(barAbsolute)) {
          throw new Error('Pressure data is stale or invalid');
        }

        const qfe = Math.round(barAbsolute / 0.029529983071445);
        state.qfe = qfe;
        state.qnh = qfe + 11;
        state.lastPressure = timestamp;
        render();
        saveSnapshot();
      } catch (error) {
        console.warn('Pressure unavailable', error);
        setFeedNote('Pressure JSON needs CORS enabled on the weather host, or a same-origin proxy if this site stops being static.');
      }
    }

    function getMqttConfig() {
      const url = new URL(mqttHost);
      const useSSL = url.protocol === 'wss:' || url.protocol === 'https:';
      return {
        host: url.hostname,
        port: Number(url.port || (useSSL ? 443 : 80)),
        path: url.pathname || '/mqtt',
        useSSL,
      };
    }

    function connectMqtt() {
      const clientId = `swansea-wx-${Math.floor(Math.random() * 100000)}`;
      const mqttConfig = getMqttConfig();
      mqttClient = new window.Paho.MQTT.Client(mqttConfig.host, mqttConfig.port, mqttConfig.path, clientId);
      mqttClient.onConnectionLost = () => {
        state.connected = false;
        setConnection('Reconnecting', 'error');
        window.setTimeout(connectMqtt, 2500);
      };
      mqttClient.onMessageArrived = (message) => {
        if (message.destinationName !== 'weather/loop') return;
        try {
          applyLoop(JSON.parse(message.payloadString));
          setFeedNote('Live data received from the airport weather feed.');
        } catch (error) {
          console.warn('Could not process weather message', error);
        }
      };
      mqttClient.connect({
        timeout: 4,
        cleanSession: true,
        useSSL: mqttConfig.useSSL,
        onSuccess: () => {
          state.connected = true;
          setConnection('Connected', 'ok');
          mqttClient.subscribe('weather/#', { qos: 0 });
        },
        onFailure: () => {
          state.connected = false;
          setConnection('Retrying', 'error');
          window.setTimeout(connectMqtt, 2500);
        },
      });
    }

    function drawRunway(centerX, centerY, angle, length, width, xShift, yShift = 0) {
      const scale = Math.min(ctx.canvas.width / 360, ctx.canvas.height / 260);
      const radians = (angle - 90) * (Math.PI / 180);
      const xStart = centerX - length * Math.cos(radians) + xShift;
      const yStart = centerY - length * Math.sin(radians) + yShift;
      const xEnd = centerX + length * Math.cos(radians) + xShift;
      const yEnd = centerY + length * Math.sin(radians) + yShift;

      ctx.strokeStyle = '#263f55';
      ctx.lineWidth = width;
      ctx.lineCap = 'round';
      ctx.beginPath();
      ctx.moveTo(xStart, yStart);
      ctx.lineTo(xEnd, yEnd);
      ctx.stroke();

      drawRunwayNumber(angle, xStart, yStart, radians, -22 * scale);
      drawRunwayNumber(angle + 180, xEnd, yEnd, radians, 22 * scale);
    }

    function drawRunwayNumber(angle, x, y, radians, extension) {
      const number = Math.round(angle / 10) % 36;
      const scale = Math.min(ctx.canvas.width / 360, ctx.canvas.height / 260);
      ctx.font = `700 ${17 * scale}px Manrope, Arial, sans-serif`;
      ctx.fillStyle = '#102233';
      ctx.textAlign = 'center';
      ctx.textBaseline = 'middle';
      ctx.fillText(String(number).padStart(2, '0'), x + extension * Math.cos(radians), y + extension * Math.sin(radians));
    }

    function drawArrow(centerX, centerY, angle) {
      const scale = Math.min(ctx.canvas.width / 360, ctx.canvas.height / 260);
      const length = 86 * scale;
      const radians = (angle + 90) * (Math.PI / 180);
      const xStart = centerX - length * Math.cos(radians);
      const yStart = centerY - length * Math.sin(radians);
      const xEnd = centerX + length * Math.cos(radians);
      const yEnd = centerY + length * Math.sin(radians);

      ctx.strokeStyle = '#a11f3a';
      ctx.fillStyle = '#a11f3a';
      ctx.lineWidth = 4 * scale;
      ctx.lineCap = 'round';
      ctx.beginPath();
      ctx.moveTo(xStart, yStart);
      ctx.lineTo(xEnd, yEnd);
      ctx.stroke();
      ctx.beginPath();
      ctx.moveTo(xEnd, yEnd);
      ctx.lineTo(xEnd - 14 * scale * Math.cos(radians - Math.PI / 6), yEnd - 14 * scale * Math.sin(radians - Math.PI / 6));
      ctx.lineTo(xEnd - 14 * scale * Math.cos(radians + Math.PI / 6), yEnd - 14 * scale * Math.sin(radians + Math.PI / 6));
      ctx.closePath();
      ctx.fill();
    }

    function drawCompass(windDirection) {
      const canvas = get('weather-compass');
      ctx = ctx || canvas.getContext('2d');
      const centerX = canvas.width / 2;
      const centerY = canvas.height / 2 + 10;
      const scale = Math.min(canvas.width / 360, canvas.height / 260);
      ctx.clearRect(0, 0, canvas.width, canvas.height);

      ctx.fillStyle = '#f8fbff';
      ctx.fillRect(0, 0, canvas.width, canvas.height);

      drawRunway(centerX, centerY, 220, 120 * scale, 13 * scale, -22 * scale);
      drawRunway(centerX, centerY, 280, 92 * scale, 9 * scale, 0, 42 * scale);

      if (Number.isFinite(windDirection)) drawArrow(centerX, centerY, windDirection);
    }

    async function init() {
      loadSnapshot();
      render();
      drawCompass(null);
      await Promise.allSettled([fetchArchive(), fetchPressure()]);
      try {
        await loadScript(MQTT_SCRIPT);
        connectMqtt();
      } catch (error) {
        console.warn('MQTT library unavailable', error);
        setConnection('Feed unavailable', 'error');
      }

      window.setInterval(render, 5000);
      window.setInterval(fetchPressure, 600000);
    }

    init();
  })();
</script>

<style>
  .weather-shell {
    --weather-gap: 0.85rem;
    --weather-pad: 1rem;
    --weather-value-size: clamp(2.65rem, 4.2vw, 4.15rem);
    --weather-secondary-size: clamp(2.25rem, 3.1vw, 3.05rem);
    display: grid;
    gap: var(--weather-gap);
    padding: 1rem 0 2rem;
  }

  .weather-head {
    display: flex;
    justify-content: space-between;
    gap: 1rem;
    align-items: end;
  }

  .weather-status {
    display: inline-flex;
    align-items: center;
    gap: 0.55rem;
    min-height: 2.5rem;
    padding: 0.5rem 0.8rem;
    border: 1px solid var(--line);
    border-radius: 999px;
    background: rgba(255, 255, 255, 0.78);
    color: var(--muted);
    font-weight: 800;
    white-space: nowrap;
  }

  .status-dot {
    width: 0.72rem;
    height: 0.72rem;
    border-radius: 50%;
    background: var(--warning);
    box-shadow: 0 0 0 0.28rem rgba(187, 104, 0, 0.14);
  }

  .status-dot.ok {
    background: #16834a;
    box-shadow: 0 0 0 0.28rem rgba(22, 131, 74, 0.14);
  }

  .status-dot.error {
    background: var(--critical);
    box-shadow: 0 0 0 0.28rem rgba(161, 31, 58, 0.14);
  }

  .weather-dashboard,
  .weather-main-column,
  .weather-side-column,
  .weather-grid {
    display: grid;
    gap: var(--weather-gap);
  }

  .weather-dashboard {
    grid-template-columns: minmax(24rem, 0.95fr) minmax(28rem, 1.05fr);
    align-items: start;
  }

  .weather-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .weather-card {
    min-width: 0;
    padding: var(--weather-pad);
    border: 1px solid var(--line);
    border-radius: 0.8rem;
    background: linear-gradient(180deg, rgba(255, 255, 255, 0.96), rgba(237, 245, 253, 0.94));
    box-shadow: var(--shadow);
  }

  .weather-wind-card {
    min-height: 8.75rem;
  }

  .weather-wind-primary {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--weather-gap);
    align-items: end;
  }

  .weather-metric-block {
    min-width: 0;
  }

  .weather-label,
  .weather-meta {
    margin: 0;
    color: var(--muted);
    font-size: 0.85rem;
    font-weight: 800;
  }

  .weather-label {
    color: var(--brand);
    text-transform: uppercase;
    letter-spacing: 0.08em;
  }

  .weather-value {
    margin: 0.35rem 0;
    font-family: 'Manrope', system-ui, sans-serif;
    font-size: var(--weather-value-size);
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    line-height: 1;
  }

  .weather-grid .weather-value {
    font-size: var(--weather-secondary-size);
  }

  .weather-pressure-card {
    grid-column: 1 / -1;
    min-height: 8.75rem;
  }

  .weather-pressure-grid {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--weather-gap);
    margin-top: 0.45rem;
  }

  .weather-pressure-label {
    margin: 0;
    color: var(--muted);
    font-size: 0.82rem;
    font-weight: 800;
    letter-spacing: 0.08em;
  }

  .weather-pressure-value {
    margin: 0.35rem 0;
    font-family: 'Manrope', system-ui, sans-serif;
    font-size: var(--weather-secondary-size);
    font-weight: 800;
    font-variant-numeric: tabular-nums;
    line-height: 1;
  }

  .weather-card-heading {
    display: flex;
    justify-content: space-between;
    gap: 1rem;
    align-items: center;
    margin-bottom: 0.75rem;
  }

  .weather-compass-card canvas {
    width: 100%;
    height: auto;
    aspect-ratio: 16 / 10;
    border: 1px solid rgba(16, 34, 51, 0.1);
    border-radius: 0.7rem;
    background: #f8fbff;
  }

  .weather-feed-note {
    margin: 0;
    color: var(--muted);
    font-weight: 700;
  }

  .weather-flash {
    animation: weather-flash 0.6s ease;
  }

  @keyframes weather-flash {
    0% {
      background: rgba(255, 224, 97, 0.8);
    }

    100% {
      background: transparent;
    }
  }

  @media (max-width: 980px) {
    .weather-shell {
      --weather-gap: 1rem;
      --weather-value-size: clamp(2.2rem, 14vw, 4rem);
      --weather-secondary-size: clamp(2rem, 12vw, 3.4rem);
    }

    .weather-head,
    .weather-dashboard,
    .weather-grid,
    .weather-main-column,
    .weather-side-column {
      grid-template-columns: 1fr;
    }

    .weather-head {
      display: grid;
      align-items: start;
    }

    .weather-wind-card {
      min-height: auto;
    }

    .weather-wind-primary,
    .weather-pressure-grid {
      grid-template-columns: 1fr;
    }
  }
</style>
