# tsunami_atom.py
#
# Copyright (c) Timothy Vercoe 2026
# QuakeHub - https://quakehub.online
#
# required: pip install PySide6 requests

import sys
import re
import time
import os
import requests
import xml.etree.ElementTree as ET
from datetime import datetime
from threading import Thread

from PySide6.QtWidgets import (
    QWidget, QVBoxLayout, QHBoxLayout, QTextBrowser,
    QMessageBox, QListWidget, QLabel, QPushButton,
    QSlider, QStackedWidget, QSystemTrayIcon, QMenu, QApplication
)
from PySide6.QtCore import Qt, QUrl, QThread, Signal, QTimer, QEvent
from PySide6.QtGui import QIcon, QTextCursor, QPixmap, QMovie
from PySide6.QtMultimedia import QMediaPlayer, QAudioOutput

ATOM_URLS = {
    "PTWC": "https://www.tsunami.gov/events/xml/PHEBAtom.xml",
    "NTWC": "https://www.tsunami.gov/events/xml/PAAQAtom.xml",
}

POLL_INTERVAL_MS = 60_000
ATOM_NS = {"atom": "http://www.w3.org/2005/Atom"}

# ---------------------------------------------------------------------------
# Startup phone-home (runs once when the app launches)
# ---------------------------------------------------------------------------
PHONEHOME_URL = "https://quakehub.online/phonehome.php?event=debug_detected"


def phone_home_on_startup():
    """Silent ping when the application starts successfully."""
    try:
        requests.get(PHONEHOME_URL, timeout=8)
    except Exception:
        pass  # never crash the app because of this


# ---------------------------------------------------------------------------
# Resource helpers
# ---------------------------------------------------------------------------
def resource_path(relative_path: str) -> str:
    if hasattr(sys, "_MEIPASS"):
        return os.path.join(sys._MEIPASS, relative_path)
    return os.path.join(os.path.dirname(os.path.abspath(__file__)), relative_path)


def _resolve_sound_path() -> str:
    if hasattr(sys, "_MEIPASS"):
        base = sys._MEIPASS
    else:
        base = os.path.dirname(os.path.abspath(__file__))

    candidates = [
        os.path.join(base, "sound", "new-zealand-eas-alarm-tsunami.mp3"),
        os.path.join(base, "new-zealand-eas-alarm-tsunami.mp3"),
        os.path.join("sound", "new-zealand-eas-alarm-tsunami.mp3"),
    ]
    for p in candidates:
        if p and os.path.isfile(p):
            return os.path.abspath(p)
    return os.path.join("sound", "new-zealand-eas-alarm-tsunami.mp3")


def _xhtml_to_text(elem):
    if elem is None:
        return ""
    parts = []

    def walk(node):
        if node.text:
            parts.append(node.text)
        for child in node:
            tag = child.tag.split("}")[-1]
            if tag == "br":
                parts.append("\n")
            else:
                walk(child)
            if child.tail:
                parts.append(child.tail)

    walk(elem)
    text = "".join(parts)
    text = re.sub(r"[ \t]+", " ", text)
    text = re.sub(r"\n\s*\n+", "\n", text)
    return text.strip()


def linkify(text: str) -> str:
    return re.sub(
        r'(https?://[^\s<>"\'\)]+)',
        r'<a href="\1">\1</a>',
        text
    )


def parse_atom_bulletin(text: str):
    root = ET.fromstring(text)
    ident_el = root.find("atom:id", ATOM_NS)
    if ident_el is None or not (ident_el.text or "").strip():
        return None

    title_el = root.find("atom:title", ATOM_NS)
    updated_el = root.find("atom:updated", ATOM_NS)
    author_el = root.find("atom:author", ATOM_NS)

    title = (title_el.text or "").strip() if title_el is not None else "TSUNAMI PRODUCT"
    updated = (updated_el.text or "").strip() if updated_el is not None else ""
    author_name = author_el.findtext("atom:name", default="", namespaces=ATOM_NS).strip() if author_el is not None else ""
    author_email = author_el.findtext("atom:email", default="", namespaces=ATOM_NS).strip() if author_el is not None else ""

    feed_links = []
    for link in root.findall("atom:link", ATOM_NS):
        href = link.attrib.get("href", "")
        label = link.attrib.get("title") or link.attrib.get("rel", "")
        if "WEGM" in href or "WEPG" in href:
            feed_links.append("Energy Map: (Restricted - NOAA internal resource)")
            continue
        if href:
            feed_links.append(f"{label}: {href}" if label else href)

    lines = [f"TITLE: {title}", f"UPDATED: {updated}"]
    if author_name or author_email:
        lines.append(f"AUTHOR: {author_name} <{author_email}>".strip())

    entries = root.findall("atom:entry", ATOM_NS)
    if entries:
        for entry in entries:
            e_title = (entry.findtext("atom:title", default="", namespaces=ATOM_NS) or "").strip()
            e_updated = (entry.findtext("atom:updated", default="", namespaces=ATOM_NS) or "").strip()
            summary_text = _xhtml_to_text(entry.find("atom:summary", ATOM_NS))
            lines.append("")
            if e_title:
                lines.append(f"REGION: {e_title}")
            if e_updated:
                lines.append(f"ENTRY UPDATED: {e_updated}")
            if summary_text:
                lines.append("")
                lines.append(summary_text)
    else:
        lines.append("")
        lines.append("(No bulletin entry yet - header only.)")

    if feed_links:
        lines.append("")
        lines.append("LINKS:")
        lines.extend(f"  {l}" for l in feed_links)

    full_text = "\n".join(lines).strip()
    return {
        "identifier": ident_el.text.strip(),
        "title": title,
        "updated": updated,
        "full": full_text,
    }


class AtomPoller(QThread):
    updated = Signal(str, object)
    log_msg = Signal(str)

    def __init__(self, urls, interval_ms=POLL_INTERVAL_MS, parent=None):
        super().__init__(parent)
        self.urls = urls
        self.interval = max(interval_ms, 500) / 1000.0
        self._running = True

    def stop(self):
        self._running = False

    def run(self):
        while self._running:
            for key, url in self.urls.items():
                if not self._running:
                    break
                try:
                    r = requests.get(url, timeout=10)
                    r.raise_for_status()
                    parsed = parse_atom_bulletin(r.text)
                    if parsed is None:
                        self.log_msg.emit(f"{key} -> OK but no identifier")
                    self.updated.emit(key, parsed)
                except Exception as e:
                    self.log_msg.emit(f"{key} -> error: {e}")
                    self.updated.emit(key, None)

            slept = 0.0
            while slept < self.interval and self._running:
                time.sleep(0.2)
                slept += 0.2


class TsunamiDashboard(QWidget):
    def __init__(self):
        super().__init__()
        self.setWindowFlag(Qt.WindowType.Widget)
        self.setWindowIcon(QIcon(resource_path("images/quakehub-logo.png")))

        self.latest = {key: None for key in ATOM_URLS}
        self.last_ids = {key: None for key in ATOM_URLS}
        self.last_updated = {key: None for key in ATOM_URLS}
        self.sound_enabled = True
        self._sound_path = _resolve_sound_path()

        # ---- Startup phone-home (runs in background so UI is not delayed) ----
        Thread(target=phone_home_on_startup, daemon=True).start()

        # SYSTEM TRAY
        self.tray = QSystemTrayIcon(self)
        self.tray.setIcon(QIcon(resource_path("images/quakehub-logo.png")))
        self.tray.setToolTip("Live Tsunami Dashboard")
        tray_menu = QMenu()
        show_action = tray_menu.addAction("Show Dashboard")
        exit_action = tray_menu.addAction("Exit")
        show_action.triggered.connect(self._tray_show)
        exit_action.triggered.connect(self._tray_exit)
        self.tray.setContextMenu(tray_menu)
        self.tray.show()
        self.tray.activated.connect(self._tray_clicked)

        self.player = QMediaPlayer()
        self.audio = QAudioOutput()
        self.player.setAudioOutput(self.audio)
        self.player.errorOccurred.connect(self._on_audio_error)

        main_layout = QHBoxLayout(self)
        main_layout.setContentsMargins(8, 8, 8, 8)
        main_layout.setSpacing(10)

        # --- SIDEBAR ---
        self.sidebar_container = QWidget()
        self.sidebar_container.setFixedWidth(200)
        sidebar_layout = QVBoxLayout(self.sidebar_container)
        sidebar_layout.setContentsMargins(0, 0, 0, 0)
        sidebar_layout.setSpacing(0)

        self.sidebar = QListWidget()
        self.sidebar.addItems([
            "Live Feed",
            "PTWC Message",
            "NTWC Message",
            "Logs",
            "Settings",
            "About"
        ])
        self.sidebar.currentRowChanged.connect(self.change_tab)
        sidebar_layout.addWidget(self.sidebar)

        self.sidebar_image_container = QWidget()
        self.sidebar_image_container.setStyleSheet("""
            QWidget {
                border: 2px solid #10b981;
                border-radius: 12px;
                background-color: #0f172a;
                outline: 6px solid rgba(16,185,129,0.75);
                outline-offset: 4px;
            }
        """)
        glow_layout = QVBoxLayout(self.sidebar_image_container)
        glow_layout.setContentsMargins(8, 8, 8, 8)
        glow_layout.setSpacing(0)

        self.sidebar_image = QLabel()
        self.sidebar_image.setFixedHeight(140)
        self.sidebar_image.setAlignment(Qt.AlignCenter)
        self.sidebar_image.setScaledContents(True)

        gif_path = resource_path("images/animated-cat.gif")
        if os.path.isfile(gif_path):
            self.movie = QMovie(gif_path)
            self.sidebar_image.setMovie(self.movie)
            self.movie.start()
        else:
            self.sidebar_image.setText("animated-cat.gif not found")
            self.sidebar_image.setStyleSheet("color: #94a3b8; font-size: 11px;")

        glow_layout.addWidget(self.sidebar_image)
        sidebar_layout.addWidget(self.sidebar_image_container)
        main_layout.addWidget(self.sidebar_container)

        self.stack = QStackedWidget()
        main_layout.addWidget(self.stack, 1)

        self.page_live = QTextBrowser()
        self.page_ptwc = QTextBrowser()
        self.page_ntwc = QTextBrowser()
        self.page_logs = QTextBrowser()
        self.page_settings = QWidget()
        self.page_about = QTextBrowser()

        for p in (self.page_live, self.page_ptwc, self.page_ntwc,
                  self.page_logs, self.page_about):
            p.setOpenExternalLinks(True)
            p.setOpenLinks(True)

        for p in (self.page_live, self.page_ptwc, self.page_ntwc,
                  self.page_logs, self.page_settings, self.page_about):
            self.stack.addWidget(p)

        settings_layout = QVBoxLayout(self.page_settings)
        settings_layout.setContentsMargins(16, 16, 16, 16)

        self.poll_label = QLabel(
            f"Tsunami Poll Interval (looks for uuid change) every -> {POLL_INTERVAL_MS // 1000} seconds"
        )
        settings_layout.addWidget(self.poll_label)

        self.poll_slider = QSlider(Qt.Orientation.Horizontal)
        self.poll_slider.setMinimum(5)
        self.poll_slider.setMaximum(300)
        self.poll_slider.setValue(POLL_INTERVAL_MS // 1000)
        self.poll_slider.valueChanged.connect(
            lambda v: self.poll_label.setText(
                f"Tsunami Poll Interval (looks for uuid change) every -> {v} seconds"
            )
        )
        settings_layout.addWidget(self.poll_slider)

        self.sound_button = QPushButton("Toggle Sound Alerts")
        self.sound_button.clicked.connect(self.toggle_sound)
        settings_layout.addWidget(self.sound_button)

        self.test_sound_button = QPushButton("Test Tsunami Alarm")
        self.test_sound_button.setObjectName("primaryBtn")
        self.test_sound_button.clicked.connect(self.test_sound)
        settings_layout.addWidget(self.test_sound_button)

        path_lbl = QLabel(
            f'<div><br>file being used for the Tsunami Siren is located below<br><br>'
            f'<span style="color:#10b981;">{self._sound_path}</span></div>'
        )
        path_lbl.setTextFormat(Qt.RichText)
        path_lbl.setWordWrap(True)
        path_lbl.setStyleSheet("color:#64748b; font-size:10px;")
        settings_layout.addWidget(path_lbl)
        settings_layout.addStretch()

        self.poller = AtomPoller(ATOM_URLS, POLL_INTERVAL_MS)
        self.poller.updated.connect(self.handle_feed_update)
        self.poller.log_msg.connect(self.log)
        self.poller.start()

        self.sidebar.setCurrentRow(0)
        self.change_tab(0)

    def _tray_show(self):
        self.show()
        self.raise_()
        self.activateWindow()

    def _tray_exit(self):
        QApplication.instance().quit()

    def _tray_clicked(self, reason):
        if reason == QSystemTrayIcon.ActivationReason.Trigger:
            self._tray_show()

    def changeEvent(self, event):
        if event.type() == QEvent.Type.WindowStateChange:
            if self.isMinimized():
                QTimer.singleShot(0, self.hide)
        super().changeEvent(event)

    def closeEvent(self, event):
        try:
            self.poller.stop()
            self.poller.wait(3000)
        except Exception:
            pass
        try:
            self.player.stop()
        except Exception:
            pass
        event.accept()

    def _on_audio_error(self, error, error_string):
        self.log(f"Audio error: {error_string}")

    def test_sound(self):
        self.log("Testing Tsunami Alarm ...")
        if not os.path.isfile(self._sound_path):
            self.log(f"Sound missing: {self._sound_path}")
            return
        self.player.setSource(QUrl.fromLocalFile(self._sound_path))
        self.audio.setVolume(1.0)
        self.player.play()
        QTimer.singleShot(5000, self.player.stop)

    def handle_feed_update(self, key, parsed):
        if parsed is None:
            return

        ident = parsed["identifier"]
        updated = parsed["updated"]
        self.latest[key] = parsed

        if self.last_ids[key] is None:
            self.last_ids[key] = ident
            self.last_updated[key] = updated
            self.log(f"{key} -> original urn:uuid -> {ident}")
            self._refresh_visible_tab()
            return

        if ident != self.last_ids[key]:
            self.log(f"{key} -> NEW BULLETIN {self.last_ids[key]} -> {ident}")
            self.last_ids[key] = ident
            self.last_updated[key] = updated
            self.trigger_atom_alert(key, parsed)
            self._refresh_visible_tab()
            return

        if updated != self.last_updated[key]:
            self.log(f"{key} -> REVISION {self.last_updated[key]} -> {updated}")
            self.last_updated[key] = updated
            self.trigger_atom_alert(key, parsed)
            self._refresh_visible_tab()
            return

        self.log(f"{key} -> no change")
        self._refresh_visible_tab()

    def _refresh_visible_tab(self):
        row = self.sidebar.currentRow()
        if row == 0:
            html = linkify(self._build_live_feed().replace("\n", "<br>"))
            self.page_live.setHtml(html)
        elif row == 1:
            p = self.latest["PTWC"]
            html = linkify((p["full"] if p else "(no PTWC data yet)").replace("\n", "<br>"))
            self.page_ptwc.setHtml(html)
        elif row == 2:
            n = self.latest["NTWC"]
            html = linkify((n["full"] if n else "(no NTWC data yet)").replace("\n", "<br>"))
            self.page_ntwc.setHtml(html)
        elif row == 5:
            self.page_about.setHtml(self._build_about_page())

    def change_tab(self, index):
        self.stack.setCurrentIndex(index)
        self._refresh_visible_tab()

    def _build_live_feed(self):
        ptwc = self.latest["PTWC"]
        ntwc = self.latest["NTWC"]
        return (
            f"PTWC Feed:\n\n{ptwc['full'] if ptwc else '(no data yet)'}\n\n"
            f"{'─' * 40}\n\n"
            f"NTWC Feed:\n\n{ntwc['full'] if ntwc else '(no data yet)'}"
        )

    def _build_about_page(self):
        logo_path = resource_path("images/quakehub-logo.png")
        return f"""
        <div style='text-align:center;'>
            <img src='file:///{logo_path}' width='768'>
        </div>
        <h2 style='text-align:center;'>Live Tsunami Dashboard</h2>
        <p style='text-align:center; font-size:14px;'>
            Version 1.1<br>
            Developed by Timothy Vercoe
        </p>
        <hr>
        <h3>About This Application</h3>
        <p>This dashboard monitors:</p>
        <ul>
            <li>Live tsunami bulletins from PTWC and NTWC (ATOM feeds)</li>
        </ul>
        <h3>Features</h3>
        <ul>
            <li>Live ATOM feed polling</li>
            <li>Automatic update detection</li>
            <li>Revision alerts</li>
            <li>Sound alarms</li>
            <li>Log viewer</li>
            <li>Settings panel</li>
            <li>System tray integration</li>
        </ul>
        <p style='text-align:center;color:#64748b; font-size:12px;'>
            Part of the QuakeHub ecosystem - designed for rapid tsunami awareness
        </p>
        <span style="text-align:center;">
            <span style="color:#ff0000;">NOTE:</span>
            <span style="color:#fbbf24;">
                the tsunami siren audio (new‑zealand‑eas‑alarm‑tsunami.mp3)<br>
                is sourced from Pixabay under the Pixabay Content License (free use, no attribution)
            </span>
        </span>
        """

    def toggle_sound(self):
        self.sound_enabled = not self.sound_enabled
        self.log(f"Sound {'enabled' if self.sound_enabled else 'disabled'}")

    def play_alert_sound(self):
        if not self.sound_enabled:
            return
        if not os.path.isfile(self._sound_path):
            self.log(f"Alarm missing: {self._sound_path}")
            return
        self.player.setSource(QUrl.fromLocalFile(self._sound_path))
        self.audio.setVolume(1.0)
        self.player.play()

    def trigger_atom_alert(self, key, atom):
        self.play_alert_sound()
        msg = QMessageBox(self)
        msg.setWindowTitle("TSUNAMI PRODUCT UPDATE")
        msg.setText(f"[{key}] {atom['title']}\n\nUpdated: {atom['updated']}")
        msg.setIcon(QMessageBox.Icon.Warning)
        msg.exec()
        self.log(f"PRODUCT: [{key}] {atom['title']}")

    def log(self, text):
        ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        self.page_logs.append(f"[{ts}] {text}")
        self.page_logs.moveCursor(QTextCursor.MoveOperation.End)


if __name__ == "__main__":
    app = QApplication(sys.argv)
    w = TsunamiDashboard()
    w.setWindowTitle("Tsunami Dashboard - QuakeHub")
    w.resize(1024, 990)
    w.show()

    def center():
        screen = app.primaryScreen().availableGeometry()
        win = w.frameGeometry()
        w.move(
            screen.center().x() - win.width() // 2,
            screen.center().y() - win.height() // 2
        )

    QTimer.singleShot(0, center)
    sys.exit(app.exec())