#!/usr/bin/env python3
from __future__ import annotations

import argparse
import csv
import json
import sys
import xml.etree.ElementTree as ET
from collections import defaultdict
from dataclasses import dataclass
from pathlib import Path
from typing import Iterable, Optional
from xml.dom import minidom

OSMAND_NS = "https://osmand.net"
ET.register_namespace("osmand", OSMAND_NS)


@dataclass
class Landmark:
    lon: float
    lat: float
    name: str
    garmin_kind: str
    garmin_type: str
    garmin_subtype: str
    gpxsee_classes: list[str]
    semantic_tags: dict[str, str]
    source_file: str


DEFAULT_GROUPS = {
    "water_sources": {"name": "Water sources", "color": "#1E88E5", "icon": "water_drop", "background": "circle"},
    "settlements": {"name": "Settlements", "color": "#43A047", "icon": "town", "background": "circle"},
    "summits": {"name": "Summits", "color": "#8E24AA", "icon": "special_star", "background": "circle"},
    "springs": {"name": "Springs", "color": "#00ACC1", "icon": "water_drop", "background": "circle"},
    "drinking_water": {"name": "Drinking water", "color": "#039BE5", "icon": "water_drop", "background": "circle"},
    "other": {"name": "Other landmarks", "color": "#FB8C00", "icon": "marker", "background": "circle"},
}


def load_landmarks(paths: Iterable[Path]) -> list[Landmark]:
    items: list[Landmark] = []
    for path in paths:
        with path.open("r", encoding="utf-8-sig", newline="") as f:
            reader = csv.DictReader(f)
            required = {"lon", "lat", "name", "garmin_kind", "garmin_type", "garmin_subtype", "gpxsee_classes_json", "semantic_tags_json"}
            missing = required - set(reader.fieldnames or [])
            if missing:
                raise ValueError(f"{path}: missing columns: {sorted(missing)}")
            for row in reader:
                try:
                    lon = float(row["lon"])
                    lat = float(row["lat"])
                except Exception:
                    continue
                name = (row.get("name") or "").strip()
                try:
                    gpxsee_classes = json.loads(row.get("gpxsee_classes_json") or "[]")
                except Exception:
                    gpxsee_classes = []
                try:
                    semantic_tags = json.loads(row.get("semantic_tags_json") or "{}")
                except Exception:
                    semantic_tags = {}
                items.append(Landmark(
                    lon=lon,
                    lat=lat,
                    name=name,
                    garmin_kind=(row.get("garmin_kind") or "").strip(),
                    garmin_type=(row.get("garmin_type") or "").strip().lower(),
                    garmin_subtype=(row.get("garmin_subtype") or "").strip().lower(),
                    gpxsee_classes=gpxsee_classes if isinstance(gpxsee_classes, list) else [],
                    semantic_tags=semantic_tags if isinstance(semantic_tags, dict) else {},
                    source_file=path.name,
                ))
    return items


def classify(item: Landmark) -> str:
    tags = item.semantic_tags
    if tags.get("amenity") == "drinking_water":
        return "drinking_water"
    if tags.get("natural") == "spring":
        return "springs"
    if tags.get("place"):
        return "settlements"
    if "summit" in item.gpxsee_classes or tags.get("natural") == "peak":
        return "summits"
    if tags.get("waterway") == "water_point" or tags.get("natural") == "water":
        return "water_sources"
    return "other"


def dedupe(items: list[Landmark], precision: int = 6) -> list[Landmark]:
    seen: set[tuple] = set()
    out: list[Landmark] = []
    for it in items:
        key = (round(it.lon, precision), round(it.lat, precision), it.name, it.garmin_type, it.garmin_subtype)
        if key in seen:
            continue
        seen.add(key)
        out.append(it)
    return out


def write_gpx(items: list[Landmark], out_path: Path, by: str = "category") -> None:
    gpx = ET.Element("gpx", {
        "version": "1.1",
        "creator": "landmarks_csv_to_osmand.py",
        "xmlns": "http://www.topografix.com/GPX/1/1",
    })
    metadata = ET.SubElement(gpx, "metadata")
    ET.SubElement(metadata, "name").text = out_path.stem

    groups_el = ET.SubElement(ET.SubElement(gpx, "extensions"), f"{{{OSMAND_NS}}}points_groups")

    def group_key(it: Landmark) -> str:
        if by == "type":
            return f"{it.garmin_type}_{it.garmin_subtype}"
        return classify(it)

    groups: dict[str, dict[str, str]] = {}
    for it in items:
        g = group_key(it)
        if g not in groups:
            if by == "type":
                groups[g] = {
                    "name": g,
                    "color": "#FB8C00",
                    "icon": "marker",
                    "background": "circle",
                }
            else:
                groups[g] = DEFAULT_GROUPS.get(g, DEFAULT_GROUPS["other"])

    for key, cfg in sorted(groups.items()):
        ET.SubElement(groups_el, f"{{{OSMAND_NS}}}group", {
            "name": key,
            "color": cfg["color"],
            "icon": cfg["icon"],
            "background": cfg["background"],
        })

    for it in items:
        wpt = ET.SubElement(gpx, "wpt", {"lat": f"{it.lat:.8f}", "lon": f"{it.lon:.8f}"})
        if it.name:
            ET.SubElement(wpt, "name").text = it.name
        ET.SubElement(wpt, "type").text = group_key(it)
        desc_parts = []
        if it.semantic_tags:
            desc_parts.append("semantic: " + json.dumps(it.semantic_tags, ensure_ascii=False, sort_keys=True))
        desc_parts.append(f"garmin: kind={it.garmin_kind} type={it.garmin_type} subtype={it.garmin_subtype}")
        if it.source_file:
            desc_parts.append(f"source={it.source_file}")
        ET.SubElement(wpt, "desc").text = "\n".join(desc_parts)
        ext = ET.SubElement(wpt, "extensions")
        cfg = groups[group_key(it)]
        ET.SubElement(ext, f"{{{OSMAND_NS}}}icon").text = cfg["icon"]
        ET.SubElement(ext, f"{{{OSMAND_NS}}}color").text = cfg["color"]
        ET.SubElement(ext, f"{{{OSMAND_NS}}}background").text = cfg["background"]

    xml_bytes = ET.tostring(gpx, encoding="utf-8")
    pretty = minidom.parseString(xml_bytes).toprettyxml(indent="  ", encoding="utf-8")
    out_path.write_bytes(pretty)


def write_osm(items: list[Landmark], out_path: Path) -> None:
    osm = ET.Element("osm", {"version": "0.6", "generator": "landmarks_csv_to_osmand.py"})
    nid = -1
    for it in items:
        node = ET.SubElement(osm, "node", {"id": str(nid), "lat": f"{it.lat:.8f}", "lon": f"{it.lon:.8f}"})
        nid -= 1
        if it.name:
            ET.SubElement(node, "tag", {"k": "name", "v": it.name})
        for k, v in sorted(it.semantic_tags.items()):
            if v is None:
                continue
            ET.SubElement(node, "tag", {"k": str(k), "v": str(v)})
        ET.SubElement(node, "tag", {"k": "garmin:kind", "v": it.garmin_kind})
        ET.SubElement(node, "tag", {"k": "garmin:type", "v": it.garmin_type})
        ET.SubElement(node, "tag", {"k": "garmin:subtype", "v": it.garmin_subtype})
        ET.SubElement(node, "tag", {"k": "source:file", "v": it.source_file})
    xml_bytes = ET.tostring(osm, encoding="utf-8")
    pretty = minidom.parseString(xml_bytes).toprettyxml(indent="  ", encoding="utf-8")
    out_path.write_bytes(pretty)


def write_summary(items: list[Landmark], out_csv: Optional[Path], out_json: Optional[Path], by: str = "category") -> None:
    counts: dict[str, int] = defaultdict(int)
    for it in items:
        key = classify(it) if by == "category" else f"{it.garmin_type}_{it.garmin_subtype}"
        counts[key] += 1
    if out_csv:
        with out_csv.open("w", encoding="utf-8", newline="") as f:
            w = csv.writer(f)
            w.writerow(["group", "count"])
            for k, v in sorted(counts.items()):
                w.writerow([k, v])
    if out_json:
        out_json.write_text(json.dumps(dict(sorted(counts.items())), ensure_ascii=False, indent=2), encoding="utf-8")


def apply_filters(items: list[Landmark], category: Optional[str], filter_tag: list[str], named_only: bool) -> list[Landmark]:
    out = items
    if category:
        out = [it for it in out if classify(it) == category]
    for expr in filter_tag:
        if "=" not in expr:
            raise ValueError(f"invalid --filter-tag value: {expr!r}; expected key=value")
        k, v = expr.split("=", 1)
        out = [it for it in out if str(it.semantic_tags.get(k)) == v]
    if named_only:
        out = [it for it in out if it.name]
    return out


def main(argv: Optional[list[str]] = None) -> int:
    ap = argparse.ArgumentParser(description="Convert landmark CSV exports into OsmAnd-friendly GPX overlay and/or OSM POI input.")
    ap.add_argument("csv", nargs="+", type=Path, help="Input landmark CSV files")
    ap.add_argument("--gpx", type=Path, help="Write OsmAnd-friendly GPX waypoint overlay")
    ap.add_argument("--osm", type=Path, help="Write OSM XML for OsmAndMapCreator generate-poi")
    ap.add_argument("--summary-csv", type=Path, help="Write category/type counts CSV")
    ap.add_argument("--summary-json", type=Path, help="Write category/type counts JSON")
    ap.add_argument("--group-by", choices=["category", "type"], default="category", help="How GPX waypoint groups should be organized")
    ap.add_argument("--summary-by", choices=["category", "type"], default="category")
    ap.add_argument("--category", help="Keep only one inferred category, e.g. water_sources, settlements, springs, drinking_water")
    ap.add_argument("--filter-tag", action="append", default=[], help="Keep only landmarks whose semantic tags contain key=value")
    ap.add_argument("--named-only", action="store_true", help="Keep only landmarks with non-empty names")
    ap.add_argument("--no-dedupe", action="store_true", help="Disable de-duplication")
    args = ap.parse_args(argv)

    items = load_landmarks(args.csv)
    items = apply_filters(items, args.category, args.filter_tag, args.named_only)
    if not args.no_dedupe:
        items = dedupe(items)

    if not any([args.gpx, args.osm, args.summary_csv, args.summary_json]):
        print(f"loaded {len(items)} landmarks", file=sys.stderr)
        return 0

    if args.gpx:
        write_gpx(items, args.gpx, by=args.group_by)
    if args.osm:
        write_osm(items, args.osm)
    if args.summary_csv or args.summary_json:
        write_summary(items, args.summary_csv, args.summary_json, by=args.summary_by)

    print(f"[info] kept {len(items)} landmarks")
    if args.gpx:
        print(f"[info] wrote GPX overlay: {args.gpx}")
    if args.osm:
        print(f"[info] wrote OSM XML: {args.osm}")
    if args.summary_csv:
        print(f"[info] wrote summary CSV: {args.summary_csv}")
    if args.summary_json:
        print(f"[info] wrote summary JSON: {args.summary_json}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
