# SPDX-License-Identifier: GPL-3.0-or-later
"""Config + wiring for the optional AIS voyage-history recorder.

Holds the small persisted config (default OFF), mints a stable per-install
``node_id``, builds the backend store factory, and provides the PostgreSQL
connection dialog. Credentials are resolved here on the GUI thread via the QGIS
auth vault and captured into the factory closure, so the recorder's worker thread
never touches QgsAuthManager (which may prompt for the master password).
"""
import json
import os
import uuid

DEFAULT_SCHEMA = "ais_watchkeeper"
STAGE_LABELS = [
    ("1  Textual (vessel information + events only)", 1),
    ("2  Voyages (a LineString per voyage, limited information)", 2),
    ("3  Full (all transponder positions)", 3),
]
BACKEND_LABELS = [("GeoPackage (local file)", "gpkg"),
                  ("PostgreSQL / PostGIS (shared)", "postgres")]


def _base_dir():
    from .guardian_store import default_base_dir
    return default_base_dir()


def config_path():
    return os.path.join(_base_dir(), "history_config.json")


def default_gpkg_path():
    return os.path.join(_base_dir(), "history", "ais_history.gpkg")


def gpkg_disk_bytes(path):
    """On-disk footprint of a GeoPackage: the file plus its WAL/SHM sidecars
    (in WAL mode most recent data lives in the -wal file until a checkpoint)."""
    total = 0
    for suffix in ("", "-wal", "-shm"):
        try:
            total += os.path.getsize(path + suffix)
        except OSError:
            pass
    return total


def human_size(n):
    """Compact human-readable byte size, e.g. 0 B, 42 KB, 3.7 MB, 1.2 GB."""
    size = float(n)
    for unit in ("B", "KB", "MB", "GB", "TB"):
        if size < 1024.0 or unit == "TB":
            return ("%d %s" % (int(size), unit) if unit == "B"
                    else "%.1f %s" % (size, unit))
        size /= 1024.0


class HistoryConfig(object):
    """Persisted recorder settings (JSON in the profile dir)."""

    def __init__(self, **kw):
        self.enabled = bool(kw.get("enabled", False))
        self.stage = int(kw.get("stage", 3))
        self.backend = kw.get("backend", "gpkg")
        self.node_id = kw.get("node_id") or uuid.uuid4().hex[:8]
        self.gpkg_path = kw.get("gpkg_path") or default_gpkg_path()
        # PostgreSQL is consumed from a connection the user has saved in the QGIS
        # Data Source Manager — we only store the connection NAME and the schema;
        # host/port/database/credentials all live in QGIS (auth vault included).
        self.pg_conn_name = kw.get("pg_conn_name", "")
        self.pg_schema = kw.get("pg_schema") or DEFAULT_SCHEMA

    def to_dict(self):
        return {k: getattr(self, k) for k in (
            "enabled", "stage", "backend", "node_id", "gpkg_path",
            "pg_conn_name", "pg_schema")}

    @classmethod
    def load(cls):
        try:
            with open(config_path(), encoding="utf-8") as fh:
                return cls(**json.load(fh))
        except Exception:                          # noqa: BLE001
            return cls()

    def save(self):
        try:
            os.makedirs(os.path.dirname(config_path()), exist_ok=True)
            with open(config_path(), "w", encoding="utf-8") as fh:
                json.dump(self.to_dict(), fh, indent=2)
        except Exception:                          # noqa: BLE001  # nosec B110
            pass

    def backend_label(self):
        return ("PostgreSQL/PostGIS '%s' (schema %s)"
                % (self.pg_conn_name or "?", self.pg_schema)
                ) if self.backend == "postgres" else (
            "GeoPackage %s" % self.gpkg_path)


def list_pg_connections():
    """Names of the PostgreSQL connections the user has saved in QGIS (Data Source
    Manager). Empty list if the provider or API is unavailable."""
    try:
        from qgis.core import QgsProviderRegistry
        md = QgsProviderRegistry.instance().providerMetadata("postgres")
        if md is None:
            return []
        return sorted(md.connections().keys())
    except Exception:                              # noqa: BLE001
        return []


def _sslmode_str(uri):
    try:
        from qgis.core import QgsDataSourceUri as U
        m = {U.SslDisable: "disable", U.SslAllow: "allow", U.SslPrefer: "prefer",
             U.SslRequire: "require", U.SslVerifyCa: "verify-ca",
             U.SslVerifyFull: "verify-full"}
        return m.get(uri.sslMode())
    except Exception:                              # noqa: BLE001
        return None


def pg_params_from_qgis(name):
    """psycopg2 connect kwargs for a saved QGIS PostgreSQL connection. Credentials
    (incl. any auth-vault config) are resolved HERE on the GUI thread, so the
    recorder's worker thread never touches QgsAuthManager."""
    from qgis.core import QgsProviderRegistry, QgsDataSourceUri
    md = QgsProviderRegistry.instance().providerMetadata("postgres")
    conn = (md.connections().get(name) if md is not None else None)
    if conn is None:
        raise ValueError("PostgreSQL connection '%s' is not defined in QGIS "
                         "(add it in the Data Source Manager)." % name)
    uri = QgsDataSourceUri(conn.uri())
    # TCP keepalives so a NAT/firewall idle-timeout can't silently drop the
    # long-lived recorder connection to a remote VPS (libpq params).
    p = {"connect_timeout": 10, "keepalives": 1, "keepalives_idle": 30,
         "keepalives_interval": 10, "keepalives_count": 5}
    if uri.host():
        p["host"] = uri.host()
    if uri.port():
        p["port"] = int(uri.port())
    if uri.database():
        p["dbname"] = uri.database()
    if uri.service():
        p["service"] = uri.service()
    sm = _sslmode_str(uri)
    if sm:
        p["sslmode"] = sm
    user, pw = uri.username(), uri.password()
    authcfg = uri.authConfigId()
    if authcfg:
        try:
            from .security_dialog import _load_credential
            u2, p2 = _load_credential(authcfg)
            user = user or u2
            pw = pw or p2
        except Exception:                          # noqa: BLE001  # nosec B110
            pass
    if user:
        p["user"] = user
    if pw:
        p["password"] = pw
    return p


def build_store_factory(cfg):
    """Return a zero-arg factory that builds the configured store ON the worker
    thread. Raises ValueError with a user-facing message if misconfigured."""
    from .history_store import GeoPackageStore, PostgresStore
    if cfg.backend == "postgres":
        if not cfg.pg_conn_name:
            raise ValueError("Choose a PostgreSQL connection "
                             "(Configure database…).")
        params = pg_params_from_qgis(cfg.pg_conn_name)   # resolve creds (GUI thread)
        schema = cfg.pg_schema

        def factory():
            return PostgresStore(dict(params), schema=schema)
        return factory
    path = cfg.gpkg_path

    def factory():
        os.makedirs(os.path.dirname(path), exist_ok=True)
        return GeoPackageStore(path)
    return factory


# --------------------------------------------------------------------------- #
#  PostgreSQL connection dialog
# --------------------------------------------------------------------------- #
def _qt():
    from qgis.PyQt.QtWidgets import (
        QDialog, QFormLayout, QLineEdit, QComboBox, QDialogButtonBox, QLabel,
        QVBoxLayout, QHBoxLayout, QPushButton, QMessageBox)
    return (QDialog, QFormLayout, QLineEdit, QComboBox, QDialogButtonBox, QLabel,
            QVBoxLayout, QHBoxLayout, QPushButton, QMessageBox)


_NO_CONN = "(no PostgreSQL connections found)"


def edit_postgres_config(cfg, parent=None):
    """Modal dialog to choose a saved QGIS PostgreSQL connection + target schema.
    Connections are managed by the user in the Data Source Manager; the plugin
    only stores the chosen connection name + schema. Returns True if accepted."""
    (QDialog, QFormLayout, QLineEdit, QComboBox, QDialogButtonBox, QLabel,
     QVBoxLayout, QHBoxLayout, QPushButton, QMessageBox) = _qt()

    dlg = QDialog(parent)
    dlg.setWindowTitle("Voyage history — PostgreSQL/PostGIS")
    root = QVBoxLayout(dlg)
    intro = QLabel(
        "Pick a PostgreSQL/PostGIS connection you have saved in QGIS. Add or edit "
        "connections in the <b>Data Source Manager ▸ PostgreSQL</b> (or the Browser "
        "panel); host, database and credentials all come from there.")
    try:
        from qgis.PyQt.QtCore import Qt
        intro.setTextFormat(Qt.RichText)
    except Exception:                              # noqa: BLE001  # nosec B110
        pass
    intro.setWordWrap(True)
    root.addWidget(intro)

    form = QFormLayout()
    combo = QComboBox()
    schema = QLineEdit(cfg.pg_schema)
    form.addRow("Connection", combo)
    form.addRow("Schema", schema)
    root.addLayout(form)

    def _repopulate():
        names = list_pg_connections()
        combo.clear()
        if names:
            combo.addItems(names)
            combo.setEnabled(True)
            if cfg.pg_conn_name in names:
                combo.setCurrentText(cfg.pg_conn_name)
        else:
            combo.addItem(_NO_CONN)
            combo.setEnabled(False)
    _repopulate()

    def _apply():
        cfg.pg_conn_name = (combo.currentText().strip()
                            if combo.isEnabled() else "")
        cfg.pg_schema = schema.text().strip() or DEFAULT_SCHEMA

    def _test():
        _apply()
        if not cfg.pg_conn_name:
            QMessageBox.warning(dlg, "Connection", "No connection selected.")
            return
        try:
            import psycopg2
            con = psycopg2.connect(**pg_params_from_qgis(cfg.pg_conn_name))
            con.close()
            QMessageBox.information(dlg, "Connection", "Connected successfully.")
        except Exception as exc:                   # noqa: BLE001
            QMessageBox.warning(dlg, "Connection failed", str(exc))

    row = QHBoxLayout()
    refresh_btn = QPushButton("Refresh list")
    refresh_btn.clicked.connect(_repopulate)
    test_btn = QPushButton("Test connection")
    test_btn.clicked.connect(_test)
    row.addWidget(refresh_btn)
    row.addWidget(test_btn)
    root.addLayout(row)

    bb = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
    bb.accepted.connect(dlg.accept)
    bb.rejected.connect(dlg.reject)
    root.addWidget(bb)

    if (dlg.exec_() if hasattr(dlg, "exec_") else dlg.exec()):
        _apply()
        return True
    return False
