"""Where an imported route came from (AV-053): the uploaded file between
its preview and the save, and -- once saved -- its record next to the
route revision it became.

* ``PendingImports``: the server's short memory of files being previewed
  (at most MAX_PENDING, each for PENDING_SECONDS). A preview answers an
  ``import_id``; the route's save names it. A server restarted in between
  forgets it: the route still saves, without the original file.
* ``journey_route_imports`` (migration 013): one row per route revision
  that came from a file -- the file's name, format, SHA-256 and bytes (the
  original, for a 1:1 export), how it was used (NEW, REPLACE, APPEND),
  whether it was matched to trails, its summary (route_import.py) and the
  points of interest the user kept. Append-only, like the revision itself:
  an edit saves a new revision without a row, and the 1:1 export is then
  no longer offered (the route is no longer the file).
"""

from __future__ import annotations

import hashlib
import json
import threading
import time
import uuid
from dataclasses import dataclass, replace
from typing import Any

from .persistence import JourneySqliteDatabase, _now, _transaction

MAX_PENDING = 16
PENDING_SECONDS = 6 * 3600
IMPORT_MODES = ("NEW", "REPLACE", "APPEND")
POI_KINDS = ("POI",)


@dataclass(frozen=True)
class PendingImport:
    import_id: str
    file_name: str
    file_format: str
    data: bytes
    summary: dict[str, Any]
    created: float
    # route_persistence's fingerprint of the points the preview served (and
    # of the matched version, once asked): a save with the same points is
    # the file unchanged.
    fingerprints: tuple[str, ...] = ()
    # AV-054: who uploaded it -- another user's preview is not there for them.
    owner: str | None = None

    @property
    def sha256(self) -> str:
        return hashlib.sha256(self.data).hexdigest()


class PendingImports:
    def __init__(self, *, clock=time.monotonic):
        self._items: dict[str, PendingImport] = {}
        self._lock = threading.Lock()
        self._clock = clock

    def add(
        self,
        file_name: str,
        file_format: str,
        data: bytes,
        summary: dict[str, Any],
        fingerprint: str,
        owner: str | None = None,
    ) -> str:
        import_id = f"import-v0_1:{uuid.uuid4()}"
        with self._lock:
            self._expire()
            while len(self._items) >= MAX_PENDING:
                oldest = min(self._items.values(), key=lambda item: item.created)
                del self._items[oldest.import_id]
            self._items[import_id] = PendingImport(
                import_id,
                file_name,
                file_format,
                data,
                summary,
                self._clock(),
                (fingerprint,),
                owner,
            )
        return import_id

    def add_fingerprint(self, import_id: str, fingerprint: str) -> None:
        with self._lock:
            item = self._items.get(import_id)
            if item is not None:
                self._items[import_id] = replace(
                    item, fingerprints=(*item.fingerprints, fingerprint)
                )

    def get(self, import_id: str, owner: str | None = None) -> PendingImport | None:
        with self._lock:
            self._expire()
            item = self._items.get(import_id)
        if item is not None and owner is not None and item.owner not in (None, owner):
            return None
        return item

    def forget_owner(self, owner: str) -> None:
        with self._lock:
            for key in [k for k, item in self._items.items() if item.owner == owner]:
                del self._items[key]

    def _expire(self) -> None:
        now = self._clock()
        for key in [k for k, item in self._items.items() if now - item.created > PENDING_SECONDS]:
            del self._items[key]


def clean_points_of_interest(raw: Any) -> list[dict[str, Any]]:
    """The points of interest the user kept from the file: name and
    position, nothing else (a value the page sends is checked, not trusted)."""
    if raw is None:
        return []
    if not isinstance(raw, list) or len(raw) > 2000:
        raise ValueError("points_of_interest must be a list (at most 2000)")
    out = []
    for item in raw:
        latitude, longitude = float(item["latitude"]), float(item["longitude"])
        if not (-90 <= latitude <= 90 and -180 <= longitude <= 180):
            raise ValueError("a point of interest has invalid coordinates")
        name = item.get("name")
        out.append(
            {
                "name": None if name is None else str(name).strip()[:200] or None,
                "latitude": latitude,
                "longitude": longitude,
                "off_route": bool(item.get("off_route")),
            }
        )
    return out


class RouteImportRepository:
    """journey_route_imports: written once per imported route revision."""

    def __init__(self, database: JourneySqliteDatabase):
        self._connection = database.connect()

    def close(self) -> None:
        self._connection.close()

    def record(
        self,
        *,
        route_revision_id: str,
        pending: PendingImport | None,
        mode: str,
        matched: bool,
        points_of_interest: list[dict[str, Any]],
        geometry_fingerprint: str,
        file_name: str | None = None,
    ) -> None:
        if mode not in IMPORT_MODES:
            raise ValueError(f"import mode must be one of {IMPORT_MODES}")
        summary = dict(pending.summary) if pending else {}
        # The saved line is the file's (or its matched version) as previewed,
        # or the user changed it before saving.
        summary["edited_before_save"] = (
            None if pending is None else geometry_fingerprint not in pending.fingerprints
        )
        with _transaction(self._connection):
            self._connection.execute(
                """INSERT INTO journey_route_imports(
                       route_revision_id, file_name, file_format, source_sha256,
                       source_bytes, mode, matched_to_trails, summary_json,
                       points_of_interest_json, created_at
                   ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
                (
                    route_revision_id,
                    pending.file_name if pending else (file_name or "plik"),
                    pending.file_format if pending else None,
                    pending.sha256 if pending else None,
                    pending.data if pending else None,
                    mode,
                    1 if matched else 0,
                    json.dumps(summary, sort_keys=True),
                    json.dumps(points_of_interest, sort_keys=True),
                    _now(),
                ),
            )

    def get(self, route_revision_id: str, *, with_bytes: bool = False) -> dict[str, Any] | None:
        row = self._connection.execute(
            "SELECT * FROM journey_route_imports WHERE route_revision_id = ?",
            (route_revision_id,),
        ).fetchone()
        if row is None:
            return None
        document = {
            "file_name": row["file_name"],
            "file_format": row["file_format"],
            "source_sha256": row["source_sha256"],
            "mode": row["mode"],
            "matched_to_trails": bool(row["matched_to_trails"]),
            "summary": json.loads(row["summary_json"]),
            "points_of_interest": json.loads(row["points_of_interest_json"]),
            "original_available": row["source_bytes"] is not None,
            "created_at": row["created_at"],
        }
        if with_bytes:
            document["source_bytes"] = row["source_bytes"]
        return document
