#!/usr/bin/env python3
"""
Berlin Tile Seeder
Lädt basemap.de Tiles für Berlin herunter und speichert sie unter
cache/tiles/{z}/{y}/{x}.png — kompatibel mit tile-proxy.php und .htaccess.

Verwendung (aus dem Projektroot-Verzeichnis ausführen):
  python3 local/seed_tiles.py                  # Dry-Run: zählt Tiles, lädt nichts
  python3 local/seed_tiles.py --download       # Startet Download Z0–Z16 (~262 MB)
  python3 local/seed_tiles.py --download --max-zoom 18   # Voll, ~4 GB, ~60 min
  python3 local/seed_tiles.py --download --min-zoom 17   # Nur Z17–Z18 nachladen
"""

import argparse
import math
import os
import sys
import time
import urllib.request
import urllib.error
from concurrent.futures import ThreadPoolExecutor
from threading import Lock

# ── Konfiguration ─────────────────────────────────────────────────────────────

BERLIN = dict(min_lon=13.08, max_lon=13.77, min_lat=52.33, max_lat=52.68)

TILE_URL = (
    'https://sgx.geodatenzentrum.de/wmts_basemapde/tile/1.0.0'
    '/de_basemapde_web_raster_grau/default/GLOBAL_WEBMERCATOR/{z}/{y}/{x}.png'
)

# ── Tile-Koordinaten ──────────────────────────────────────────────────────────

def lat_lon_to_tile(lat, lon, z):
    n = 1 << z
    x = int((lon + 180) / 360 * n)
    lr = math.radians(lat)
    y = int((1 - math.log(math.tan(lr) + 1 / math.cos(lr)) / math.pi) / 2 * n)
    return x, y


def bbox_tiles(bbox, z):
    x0, y0 = lat_lon_to_tile(bbox['max_lat'], bbox['min_lon'], z)
    x1, y1 = lat_lon_to_tile(bbox['min_lat'], bbox['max_lon'], z)
    for y in range(y0, y1 + 1):
        for x in range(x0, x1 + 1):
            yield (z, y, x)

# ── Download ──────────────────────────────────────────────────────────────────

def download_tile(z, y, x, cache_dir, delay):
    path = os.path.join(cache_dir, str(z), str(y), f'{x}.png')
    if os.path.isfile(path):
        return 'skip'
    os.makedirs(os.path.dirname(path), exist_ok=True)
    url = TILE_URL.format(z=z, y=y, x=x)
    for attempt in range(3):
        try:
            req = urllib.request.Request(url, headers={'User-Agent': 'tile-seeder/1.0'})
            with urllib.request.urlopen(req, timeout=15) as resp:
                data = resp.read()
            tmp = path + '.tmp'
            with open(tmp, 'wb') as f:
                f.write(data)
            os.replace(tmp, path)
            time.sleep(delay)
            return 'ok'
        except KeyboardInterrupt:
            raise
        except Exception as e:
            if attempt < 2:
                time.sleep(2 ** attempt)
            else:
                return f'err:{e}'
    return 'err'

# ── Hauptprogramm ─────────────────────────────────────────────────────────────

def main():
    ap = argparse.ArgumentParser(description='Berlin Tile Seeder für basemap.de')
    ap.add_argument('--download',  action='store_true', help='Tiles herunterladen (ohne: nur Dry-Run)')
    ap.add_argument('--cache-dir', default='cache/tiles', help='Zielverzeichnis (default: cache/tiles)')
    ap.add_argument('--min-zoom',  type=int, default=0,  help='Minimaler Zoomlevel (default: 0)')
    ap.add_argument('--max-zoom',  type=int, default=16, help='Maximaler Zoomlevel (default: 16)')
    ap.add_argument('--workers',   type=int, default=4,  help='Parallele Downloads (default: 4)')
    ap.add_argument('--delay',     type=float, default=0.05,
                    help='Pause je Worker zwischen Requests in s (default: 0.05)')
    args = ap.parse_args()

    # Tile-Liste aufbauen und Übersicht zeigen
    all_tiles = []
    print(f'\nBerlin Tile Seeder  —  Z{args.min_zoom}–Z{args.max_zoom}')
    print(f'{"Zoom":>6}  {"Tiles":>9}  {"~MB":>8}  {"~Min":>6}')
    print('─' * 36)
    rate = args.workers / args.delay  # tiles/s (rough)
    for z in range(args.min_zoom, args.max_zoom + 1):
        tiles = list(bbox_tiles(BERLIN, z))
        mb    = len(tiles) * 15 / 1024
        mins  = len(tiles) / rate / 60
        print(f'  Z{z:<4}  {len(tiles):>9,}  {mb:>8.1f}  {mins:>6.1f}')
        all_tiles.extend(tiles)

    total_mb   = len(all_tiles) * 15 / 1024
    total_mins = len(all_tiles) / rate / 60
    print('─' * 36)
    print(f'  {"Σ":>4}  {len(all_tiles):>9,}  {total_mb:>8.0f}  {total_mins:>6.1f}\n')

    if not args.download:
        print('Dry-Run — kein Download. Mit --download starten.\n')
        return

    print(f'Download → {args.cache_dir}  ({args.workers} Workers, {args.delay}s Delay)\n')

    lock   = Lock()
    counts = {'ok': 0, 'skip': 0, 'err': 0}
    done   = [0]
    total  = len(all_tiles)
    t0     = time.time()

    def run(tile):
        status = download_tile(*tile, args.cache_dir, args.delay)
        with lock:
            key = status if status in counts else 'err'
            counts[key] += 1
            done[0] += 1
            n = done[0]
            if n % 200 == 0 or n == total:
                elapsed = time.time() - t0 or 0.001
                spd     = n / elapsed
                eta     = (total - n) / spd if spd else 0
                pct     = n / total * 100
                print(
                    f'\r  {pct:5.1f}%  {n:,}/{total:,}  '
                    f'ok:{counts["ok"]:,}  skip:{counts["skip"]:,}  '
                    f'err:{counts["err"]}  '
                    f'{spd:.0f}/s  ETA {eta/60:.0f} min   ',
                    end='', flush=True
                )

    try:
        with ThreadPoolExecutor(max_workers=args.workers) as ex:
            list(ex.map(run, all_tiles))
    except KeyboardInterrupt:
        print('\n\nAbgebrochen.')
        sys.exit(1)

    elapsed = time.time() - t0
    print(
        f'\n\nFertig in {elapsed/60:.1f} min  —  '
        f'geladen: {counts["ok"]:,}  '
        f'übersprungen: {counts["skip"]:,}  '
        f'Fehler: {counts["err"]}'
    )
    if counts['err']:
        print('Tipp: Skript erneut ausführen — fehlgeschlagene Tiles werden nachgeladen.')


if __name__ == '__main__':
    main()
