#!/usr/bin/env python3
"""
immich_geotag_from_owntracks_wrong.py

Geotaguj zdjęcia na podstawie śladu z OwnTracks (JSON) lub pliku GPX
z wykorzystaniem exiftool. Idealne do późniejszego importu / reindeksu w Immich.

Wymagania:
- Python 3.9+
- exiftool (musi być w PATH, np. `apt-get install exiftool` lub pakiet systemowy)
- (Opcjonalnie) uprawnienia do zapisu plików JPG/HEIC/PNG itp.

Przykłady:
1) JSON -> GPX -> geotag (rekurencyjnie po katalogu ze zdjęciami):
   python3 immich_geotag_from_owntracks_wrong.py \
     --photos /ścieżka/do/zdjęć \
     --json /ścieżka/do/owntracks_json \
     --json-max-age-secs 10 \
     --interpolate-secs 180 \
     --time-offset "+00:00" \
     --write inplace

2) GPX -> geotag:
   python3 immich_geotag_from_owntracks_wrong.py \
     --photos /ścieżka/do/zdjęć \
     --gpx /ścieżka/track.gpx \
     --interpolate-secs 180 \
     --time-offset "+00:00" \
     --write inplace

Po zakończeniu możesz wykonać reindeks w Immich, aby wczytać nowe EXIF GPS.
"""

import argparse
import json
import os
import subprocess
import sys
import tempfile
from datetime import datetime, timezone
from pathlib import Path
from typing import List, Dict, Any, Optional

def parse_args() -> argparse.Namespace:
    p = argparse.ArgumentParser(description="Geotaguj zdjęcia na podstawie OwnTracks JSON lub GPX (dla Immich).")
    g_input = p.add_argument_group("Wejście śladu")
    g_input.add_argument("--gpx", type=str, help="Ścieżka do pliku GPX (opcjonalnie zamiast --json).")
    g_input.add_argument("--json", type=str, help="Ścieżka do pliku JSON OwnTracks lub katalogu z JSON-ami.")
    g_input.add_argument("--json-glob", type=str, default="*.json", help="Wzorzec plików JSON gdy --json wskazuje katalog (domyślnie: *.json).")

    g_match = p.add_argument_group("Parametry dopasowania czasu")
    g_match.add_argument("--interpolate-secs", type=int, default=180, help="Maksymalne okno interpolacji (GeoMaxIntSecs) w sekundach (domyślnie 180).")
    g_match.add_argument("--json-max-age-secs", type=int, default=10, help="Ignoruj punkty JSON bez timestampu lub dane starsze/niepoprawne (bufor bezpieczeństwa).")
    g_match.add_argument("--time-offset", type=str, default="+00:00",
                         help="Korekta czasu EXIF względem GPX, np. '+00:00', '+01:00', '-00:30'. Przekazywana do exiftool jako -geosync.")

    g_photos = p.add_argument_group("Zdjęcia")
    g_photos.add_argument("--photos", type=str, required=True, help="Katalog lub plik ze zdjęciem(i).")
    g_photos.add_argument("--recursive", action="store_true", help="Przetwarzaj rekursywnie katalog zdjęć (-r dla exiftool).")

    g_out = p.add_argument_group("Zapis")
    g_out.add_argument("--write", choices=["inplace", "backup"], default="inplace",
                       help="Tryb zapisu: 'inplace' (overwrite_original) lub 'backup' (domyślny tryb exiftool z plikami *_original).")
    g_out.add_argument("--dry-run", action="store_true", help="Nie zapisuj EXIF, tylko pokaż co byłoby wykonane.")

    g_misc = p.add_argument_group("Różne")
    g_misc.add_argument("--tmpdir", type=str, help="Katalog tymczasowy na plik GPX (jeśli generowany z JSON).")
    g_misc.add_argument("--verbose", action="store_true", help="Więcej logów.")

    return p.parse_args()

def discover_json_files(path: Path, pattern: str) -> List[Path]:
    if path.is_file():
        return [path]
    elif path.is_dir():
        return sorted(path.rglob(pattern))
    else:
        raise FileNotFoundError(f"Ścieżka JSON nie istnieje: {path}")

def load_owntracks_points(json_files: List[Path], max_age_seconds: int, verbose=False) -> List[Dict[str, Any]]:
    """
    Wczytuje punkty OwnTracks z plików JSON.
    Oczekuje kluczy: lat, lon, tst (epoch sekundy). Toleruje inne pola.
    Zwraca listę posortowaną po czasie (UTC).
    """
    points = []
    for f in json_files:
        try:
            with open(f, "r", encoding="utf-8") as fh:
                data = json.load(fh)
        except Exception as e:
            if verbose:
                print(f"[WARN] Nie udało się wczytać {f}: {e}", file=sys.stderr)
            continue

        # Obsługa: plik zawiera pojedynczy obiekt lub listę obiektów
        entries = data if isinstance(data, list) else [data]
        for entry in entries:
            try:
                lat = float(entry["lat"])
                lon = float(entry["lon"])
                tst = int(entry["tst"])  # epoch (sekundy)
                if tst <= 0:
                    continue
                dt = datetime.fromtimestamp(tst, tz=timezone.utc)
                points.append({"lat": lat, "lon": lon, "time": dt})
            except Exception:
                continue

    points.sort(key=lambda x: x["time"])

    if verbose:
        if points:
            print(f"[INFO] Załadowano {len(points)} punktów OwnTracks. Zakres: {points[0]['time']} .. {points[-1]['time']} (UTC)")
        else:
            print("[WARN] Nie znaleziono żadnych punktów OwnTracks w JSON.")

    return points

def write_gpx(points: List[Dict[str, Any]], out_path: Path, verbose=False) -> None:
    """
    Minimalny zapis GPX 1.1 z jedną trasą (trk) i jednym odcinkiem (trkseg).
    """
    def iso8601(dt: datetime) -> str:
        return dt.astimezone(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")

    with open(out_path, "w", encoding="utf-8") as gpx:
        gpx.write('<?xml version="1.0" encoding="UTF-8"?>\n')
        gpx.write('<gpx version="1.1" creator="immich_geotag_from_owntracks_wrong.py" xmlns="http://www.topografix.com/GPX/1/1">\n')
        gpx.write('  <trk>\n')
        gpx.write('    <name>OwnTracks Export</name>\n')
        gpx.write('    <trkseg>\n')
        for pt in points:
            gpx.write(f'      <trkpt lat="{pt["lat"]:.8f}" lon="{pt["lon"]:.8f}"><time>{iso8601(pt["time"])}</time></trkpt>\n')
        gpx.write('    </trkseg>\n')
        gpx.write('  </trk>\n')
        gpx.write('</gpx>\n')

    if verbose:
        print(f"[INFO] Zapisano GPX: {out_path} ({len(points)} punktów)")

def ensure_exiftool_available() -> None:
    try:
        subprocess.run(["exiftool", "-ver"], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    except (subprocess.CalledProcessError, FileNotFoundError):
        print("[ERROR] exiftool nie jest dostępny w PATH. Zainstaluj exiftool i spróbuj ponownie.", file=sys.stderr)
        sys.exit(2)

def build_exiftool_cmd(gpx_path: Path, photos_path: Path, interpolate_secs: int, time_offset: str,
                       recursive: bool, write_mode: str, dry_run: bool) -> List[str]:
    cmd = ["exiftool"]
    if recursive and photos_path.is_dir():
        cmd.append("-r")

    cmd.append("-api")
    cmd.append(f"GeoMaxIntSecs={interpolate_secs}")

    if time_offset:
        cmd.append(f"-geosync={time_offset}")

    cmd.append("-geotag")
    cmd.append(str(gpx_path))

    if write_mode == "inplace" and not dry_run:
        cmd.append("-overwrite_original")

    cmd.append(str(photos_path))

    if dry_run:
        cmd.insert(1, "-n")
        cmd.append("-S")
        cmd.append("-v2")

    return cmd

def run_exiftool(cmd: List[str], verbose=False) -> int:
    if verbose:
        print("[INFO] Uruchamiam:", " ".join(cmd))
    try:
        proc = subprocess.run(cmd, check=False)
        return proc.returncode
    except FileNotFoundError:
        print("[ERROR] exiftool nie znaleziony.", file=sys.stderr)
        return 127

def main() -> int:
    args = parse_args()
    ensure_exiftool_available()

    photos_path = Path(args.photos)
    if not photos_path.exists():
        print(f"[ERROR] Ścieżka do zdjęć nie istnieje: {photos_path}", file=sys.stderr)
        return 1

    gpx_path: Optional[Path] = None
    temp_dir = None

    if args.gpx:
        gpx_path = Path(args.gpx)
        if not gpx_path.exists():
            print(f"[ERROR] Plik GPX nie istnieje: {gpx_path}", file=sys.stderr)
            return 1
        if args.verbose:
            print(f"[INFO] Używam dostarczonego GPX: {gpx_path}")
    else:
        if not args.json:
            print("[ERROR] Musisz podać --gpx lub --json.", file=sys.stderr)
            return 1

        json_path = Path(args.json)
        try:
            files = discover_json_files(json_path, args.json_glob)
        except FileNotFoundError as e:
            print(f"[ERROR] {e}", file=sys.stderr)
            return 1

        points = load_owntracks_points(files, args.json_max_age_secs, verbose=args.verbose)
        if not points:
            print("[ERROR] Brak punktów z OwnTracks po wczytaniu JSON.", file=sys.stderr)
            return 1

        temp_dir = Path(args.tmpdir) if args.tmpdir else Path(tempfile.mkdtemp(prefix="owntracks_gpx_"))
        temp_dir.mkdir(parents=True, exist_ok=True)
        gpx_path = temp_dir / "owntracks_export.gpx"
        write_gpx(points, gpx_path, verbose=args.verbose)

    assert gpx_path is not None

    cmd = build_exiftool_cmd(
        gpx_path=gpx_path,
        photos_path=photos_path,
        interpolate_secs=args.interpolate_secs,
        time_offset=args.time_offset,
        recursive=args.recursive,
        write_mode=args.write,
        dry_run=args.dry_run,
    )

    rc = run_exiftool(cmd, verbose=args.verbose)
    if rc != 0:
        print(f"[ERROR] exiftool zakończył się kodem {rc}.", file=sys.stderr)
        return rc

    if args.dry_run:
        print("[OK] Dry-run zakończony. Wygląda dobrze. Usuń --dry-run, aby zapisać EXIF.")
    else:
        print("[OK] Geotagowanie zakończone sukcesem. Uruchom reindeks w Immich, aby wczytać GPS.")

    if temp_dir:
        print(f"[INFO] Tymczasowy GPX: {gpx_path} (możesz go zachować do archiwum).")

    return 0

if __name__ == "__main__":
    sys.exit(main())
