"""Persistent per-MMSI vessel store (a small learned database).

When a static message gives a vessel identity/dimension data, it is written
here, so on later sightings — including in future sessions, before a fresh
static message has arrived — the plugin can already label and size that MMSI.
A static message always takes precedence and refreshes the stored values.

Stored as a simple XML file in the plugin folder:

    <vessels>
      <vessel mmsi="211620610" name="ASTERODE" length="89" beam="13" ship_type="70"/>
      <vessel mmsi="351759000" name="EVER DIADEM" length="295" beam="32" ship_type="70"/>
    </vessels>

Older files that only carry mmsi+name still load fine (missing attributes are
simply absent). Accessed only from the GUI thread, so no locking is needed.
Writes are best-effort: a read-only install directory degrades to an in-memory
cache rather than raising.
"""
import os
import shutil
import xml.etree.ElementTree as ET  # nosec B405

# Older versions kept the file inside the plugin folder, which QGIS wipes on
# update. Keep it now in the persistent profile folder (the same place the
# guard-zones / coverage GeoPackages live) so it survives plugin updates.
_LEGACY_XML_PATH = os.path.join(os.path.dirname(__file__), "vessel_names.xml")


def _default_xml_path():
    """Path to the registry XML in the persistent profile folder (survives plugin
    upgrades), creating the folder; falls back to a home-dir folder off QGIS."""
    try:
        from .guardian_store import default_base_dir
        base = default_base_dir()
    except Exception:
        base = os.path.join(os.path.expanduser("~"), ".qgis_ais_guardian",
                            "ais_nmea_source")
    try:
        os.makedirs(base, exist_ok=True)
    except Exception:  # nosec B110
        pass
    return os.path.join(base, "vessel_names.xml")

# Attributes persisted per vessel and how to parse each back from XML text.
_PARSERS = {
    "name": lambda s: s.strip() or None,
    "callsign": lambda s: s.strip() or None,
    "imo": int,
    "length": float,
    "beam": float,
    "ship_type": int,
    "ship_type_desc": lambda s: s.strip() or None,
}
_FIELDS = tuple(_PARSERS)  # name, callsign, imo, length, beam, ship_type, ship_type_desc


class VesselRegistry:
    def __init__(self, path=None):
        """Open the registry at `path` (or the default), carrying over any legacy
        in-plugin file once, then load the stored records into memory."""
        self._path = path or _default_xml_path()
        self._migrate_legacy()
        self._rec = {}          # mmsi(str) -> {field: value}
        self._load()

    def _migrate_legacy(self):
        """One-time carry-over: if an older version left the registry inside the
        plugin folder and the persistent copy doesn't exist yet, move it across
        so the learned database isn't lost on the upgrade."""
        try:
            if (self._path != _LEGACY_XML_PATH
                    and not os.path.exists(self._path)
                    and os.path.exists(_LEGACY_XML_PATH)):
                shutil.copyfile(_LEGACY_XML_PATH, self._path)
        except Exception:  # nosec B110
            pass

    def _load(self):
        """Parse the XML file into the in-memory record map; on any read/parse
        error start empty. Unknown/blank attributes are skipped per field."""
        try:
            from .xml_safe import parse_root
            root = parse_root(self._path)
        except Exception:
            return  # missing/unreadable/unsafe -> start empty
        for el in root.findall("vessel"):
            mmsi = el.get("mmsi")
            if not mmsi:
                continue
            record = {}
            for field, parse in _PARSERS.items():
                raw = el.get(field)
                if raw is None or raw == "":
                    continue
                try:
                    val = parse(raw)
                except Exception:  # nosec B112
                    continue
                if val is not None:
                    record[field] = val
            if record:
                self._rec[mmsi] = record

    # ---- reads -----------------------------------------------------------
    def get(self, mmsi):
        """Last-known name for an MMSI (kept for backwards compatibility)."""
        return self._rec.get(str(mmsi), {}).get("name")

    def get_record(self, mmsi):
        """Full last-known record dict for an MMSI (empty dict if unknown)."""
        return dict(self._rec.get(str(mmsi), {}))

    def records(self):
        """All stored vessels as {mmsi: record dict} — backs the database view."""
        return {m: dict(r) for m, r in self._rec.items()}

    # ---- writes ----------------------------------------------------------
    def set(self, mmsi, name=None, callsign=None, imo=None, length=None,
            beam=None, ship_type=None, ship_type_desc=None):
        """Merge any supplied fields into the stored record.

        Only non-None values are applied, so a Type 24 Part A (name) and Part B
        (type/size) accumulate. Persists only when something actually changed.
        """
        mmsi = str(mmsi)
        if not mmsi:
            return
        incoming = {"name": name, "callsign": callsign, "imo": imo,
                    "length": length, "beam": beam, "ship_type": ship_type,
                    "ship_type_desc": ship_type_desc}
        record = dict(self._rec.get(mmsi, {}))
        changed = False
        for field, val in incoming.items():
            if val is None:
                continue
            if field in ("name", "callsign", "ship_type_desc"):
                val = str(val).strip()
                if not val:
                    continue
            if record.get(field) != val:
                record[field] = val
                changed = True
        if changed:
            self._rec[mmsi] = record
            self._save()

    def _save(self):
        """Write the whole registry back to XML (pretty-printed). Best-effort:
        a read-only directory is swallowed so the in-memory cache keeps working."""
        root = ET.Element("vessels")
        for mmsi in sorted(self._rec):
            attrs = {"mmsi": mmsi}
            for field in _FIELDS:
                val = self._rec[mmsi].get(field)
                if val is not None:
                    # store ints without a trailing ".0"
                    if field in ("length", "beam") and float(val).is_integer():
                        attrs[field] = str(int(val))
                    else:
                        attrs[field] = str(val)
            ET.SubElement(root, "vessel", **attrs)
        tree = ET.ElementTree(root)
        try:
            ET.indent(tree, space="  ")   # pretty-print (Python 3.9+)
        except Exception:  # nosec B110
            pass
        try:
            tree.write(self._path, encoding="utf-8", xml_declaration=True)
        except Exception:  # nosec B110
            pass  # read-only dir etc. — keep running with the in-memory cache

    def count(self):
        """Number of vessels currently in the registry."""
        return len(self._rec)
