# -*- coding: utf-8 -*-
"""Přesný převod EPSG:5514 ↔ EPSG:4326 s mřížkami ČÚZK (ČR) a UGKK (SR)."""
import io
import os
import shutil
import webbrowser
import zipfile

import requests

from qgis.PyQt.QtCore import QSettings, Qt
from qgis.PyQt.QtWidgets import QMessageBox

try:
    _RICH = Qt.TextFormat.RichText
except AttributeError:
    _RICH = Qt.RichText

try:
    _BTN_OK = QMessageBox.StandardButton.Ok
    _ROLE_ACTION = QMessageBox.ButtonRole.ActionRole
except AttributeError:
    _BTN_OK = QMessageBox.Ok
    _ROLE_ACTION = QMessageBox.ActionRole

URL_PROJ_CDN = "https://cdn.proj.org/"
URL_CZ_TIF = "https://cdn.proj.org/cz_cuzk_table_-y-x_3_v1710.tif"
URL_CZ_ZIP = "https://geoportal.cuzk.gov.cz/dokumenty/table_-y-x_3_v1710.tif.zip"
URL_CZ_INFO = "https://geoportal.cuzk.cz/"
URL_SK_TIF = "https://cdn.proj.org/sk_gku_JTSK03_to_JTSK.tif"
URL_SK_PORTAL = "https://www.geoportal.sk/sk/geodeticke-zaklady/na-stiahnutie/"

GRID_CZ = "cz_cuzk_table_-y-x_3_v1710.tif"
GRID_SK = "sk_gku_JTSK03_to_JTSK.tif"
MIN_GRID_BYTES = 20000

# Záložní celostátní Helmert (EPSG:1623), ~1 m, bez lokálních deformací.
HELMERT_5514_TO_4326 = (
    "+proj=pipeline "
    "+step +inv +proj=krovak +lat_0=49.5 +lon_0=24.8333333333333 "
    "+alpha=30.2881397527778 +k=0.9999 +x_0=0 +y_0=0 +ellps=bessel "
    "+step +proj=push +v_3 +step +proj=cart +ellps=bessel "
    "+step +proj=helmert +x=570.8 +y=85.7 +z=462.8 +rx=4.998 +ry=1.587 +rz=5.261 "
    "+s=3.56 +convention=position_vector "
    "+step +inv +proj=cart +ellps=WGS84 +step +proj=pop +v_3 "
    "+step +proj=unitconvert +xy_in=rad +xy_out=deg"
)

HELMERT_4326_TO_5514 = (
    "+proj=pipeline "
    "+step +proj=unitconvert +xy_in=deg +xy_out=rad "
    "+step +proj=push +v_3 +step +proj=cart +ellps=WGS84 "
    "+step +inv +proj=helmert +x=570.8 +y=85.7 +z=462.8 +rx=4.998 +ry=1.587 +rz=5.261 "
    "+s=3.56 +convention=position_vector "
    "+step +inv +proj=cart +ellps=bessel +step +proj=pop +v_3 "
    "+step +proj=krovak +lat_0=49.5 +lon_0=24.8333333333333 "
    "+alpha=30.2881397527778 +k=0.9999 +x_0=0 +y_0=0 +ellps=bessel"
)

# ČÚZK: 5514 → mřížka → S-JTSK/05 → Helmert EPSG:5226 → ETRS89/WGS 84.
def _cz_5514_to_4326_op():
    grid = _proj_grid_grids_param(GRID_CZ)
    return (
        "+proj=pipeline "
        "+step +proj=gridshift +grids=" + grid + " "
        "+step +inv +proj=mod_krovak +lat_0=49.5 +lon_0=24.8333333333333 "
        "+alpha=30.2881397222222 +k=0.9999 +x_0=5000000 +y_0=5000000 +ellps=bessel "
        "+step +proj=push +v_3 +step +proj=cart +ellps=bessel "
        "+step +proj=helmert +x=572.213 +y=85.334 +z=461.94 "
        "+rx=-4.9732 +ry=-1.529 +rz=-5.2484 +s=3.5378 +convention=coordinate_frame "
        "+step +inv +proj=cart +ellps=GRS80 +step +proj=pop +v_3 "
        "+step +proj=unitconvert +xy_in=rad +xy_out=deg"
    )


def _cz_4326_to_5514_op():
    grid = _proj_grid_grids_param(GRID_CZ)
    return (
        "+proj=pipeline "
        "+step +proj=unitconvert +xy_in=deg +xy_out=rad "
        "+step +proj=push +v_3 +step +proj=cart +ellps=GRS80 "
        "+step +inv +proj=helmert +x=572.213 +y=85.334 +z=461.94 "
        "+rx=-4.9732 +ry=-1.529 +rz=-5.2484 +s=3.5378 +convention=coordinate_frame "
        "+step +inv +proj=cart +ellps=bessel +step +proj=pop +v_3 "
        "+step +proj=mod_krovak +lat_0=49.5 +lon_0=24.8333333333333 "
        "+alpha=30.2881397222222 +k=0.9999 +x_0=5000000 +y_0=5000000 +ellps=bessel "
        "+step +inv +proj=gridshift +grids=" + grid
    )


def _sk_5514_to_4326_op():
    grid = _proj_grid_grids_param(GRID_SK)
    return (
        "+proj=pipeline "
        "+step +inv +proj=krovak +lat_0=49.5 +lon_0=24.8333333333333 "
        "+alpha=30.2881397527778 +k=0.9999 +x_0=0 +y_0=0 +ellps=bessel "
        "+step +inv +proj=hgridshift +grids=" + grid + " "
        "+step +proj=push +v_3 +step +proj=cart +ellps=bessel "
        "+step +proj=helmert +x=485.021 +y=169.465 +z=483.839 "
        "+rx=-7.786342 +ry=-4.397554 +rz=-4.102655 +s=0 +convention=coordinate_frame "
        "+step +inv +proj=cart +ellps=GRS80 +step +proj=pop +v_3 "
        "+step +proj=unitconvert +xy_in=rad +xy_out=deg"
    )


def _sk_4326_to_5514_op():
    grid = _proj_grid_grids_param(GRID_SK)
    return (
        "+proj=pipeline "
        "+step +proj=unitconvert +xy_in=deg +xy_out=rad "
        "+step +proj=push +v_3 +step +proj=cart +ellps=GRS80 "
        "+step +inv +proj=helmert +x=485.021 +y=169.465 +z=483.839 "
        "+rx=-7.786342 +ry=-4.397554 +rz=-4.102655 +s=0 +convention=coordinate_frame "
        "+step +inv +proj=cart +ellps=bessel +step +proj=pop +v_3 "
        "+step +proj=hgridshift +grids=" + grid + " "
        "+step +proj=krovak +lat_0=49.5 +lon_0=24.8333333333333 "
        "+alpha=30.2881397527778 +k=0.9999 +x_0=0 +y_0=0 +ellps=bessel"
    )


_GRIDS_READY = False
_GUIDE_SHOWN = False


def grids_dir():
    plugin_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
    d = os.path.join(plugin_root, "grids")
    os.makedirs(d, exist_ok=True)
    return d


def _grid_path(name):
    return os.path.join(grids_dir(), name)


def _grid_ok(name):
    path = _grid_path(name)
    return os.path.isfile(path) and os.path.getsize(path) >= MIN_GRID_BYTES


def _proj_grid_grids_param(name):
    """Absolutní cesta k mřížce pro PROJ pipeline (forward slashes)."""
    if _grid_ok(name):
        path = _grid_path(name).replace("\\", "/")
        if " " in path:
            return "'" + path + "'"
        return path
    return name


def _qgis_profile_proj_dir():
    try:
        from qgis.core import QgsApplication
        return os.path.join(QgsApplication.qgisSettingsDirPath(), "proj")
    except Exception:
        return None


def _sync_grid_to_qgis_profile(name):
    """Zkopíruje mřížku do profile/proj — QGIS ji pak najde i bez vlastní pipeline."""
    if not _grid_ok(name):
        return
    dest_dir = _qgis_profile_proj_dir()
    if not dest_dir:
        return
    try:
        os.makedirs(dest_dir, exist_ok=True)
        dest = os.path.join(dest_dir, name)
        src = _grid_path(name)
        if (not os.path.isfile(dest)) or os.path.getsize(dest) < os.path.getsize(src):
            shutil.copy2(src, dest)
    except Exception:
        pass


def _sync_grids_to_qgis_profile():
    for name in (GRID_CZ, GRID_SK):
        _sync_grid_to_qgis_profile(name)


def _register_proj_search_path():
    d = grids_dir()
    profile_proj = _qgis_profile_proj_dir()
    search_dirs = [d]
    if profile_proj and os.path.isdir(profile_proj):
        search_dirs.append(profile_proj)
    try:
        from osgeo import osr
        paths = list(osr.GetPROJSearchPaths() or [])
        for search_dir in search_dirs:
            if search_dir not in paths:
                paths.insert(0, search_dir)
        if paths:
            osr.SetPROJSearchPaths(paths)
    except Exception:
        pass
    try:
        from pyproj import datadir
        for search_dir in search_dirs:
            datadir.append_data_dir(search_dir)
    except Exception:
        pass
    for key in ("PROJ_DATA", "PROJ_LIB"):
        cur = os.environ.get(key, "")
        parts = cur.split(os.pathsep) if cur else []
        for search_dir in reversed(search_dirs):
            if search_dir not in parts:
                parts.insert(0, search_dir)
        if parts:
            os.environ[key] = os.pathsep.join(parts)


def _download_file(url, dest, timeout=45):
    r = requests.get(url, timeout=timeout, stream=True)
    r.raise_for_status()
    tmp = dest + ".part"
    with open(tmp, "wb") as f:
        for chunk in r.iter_content(chunk_size=65536):
            if chunk:
                f.write(chunk)
    if os.path.getsize(tmp) < MIN_GRID_BYTES:
        os.remove(tmp)
        raise IOError("Stažený soubor je příliš malý: " + url)
    if os.path.exists(dest):
        os.remove(dest)
    os.rename(tmp, dest)


def _download_grid(name, urls):
    dest = _grid_path(name)
    last_err = None
    for url in urls:
        try:
            if url.lower().endswith(".zip"):
                _download_grid_zip(url, dest)
            else:
                _download_file(url, dest)
            return True
        except Exception as e:
            last_err = e
    if last_err:
        raise last_err
    return False


def _download_grid_zip(url, dest):
    r = requests.get(url, timeout=90, stream=True)
    r.raise_for_status()
    with zipfile.ZipFile(io.BytesIO(r.content)) as zf:
        tif_names = [n for n in zf.namelist() if n.lower().endswith(".tif")]
        if not tif_names:
            raise IOError("ZIP neobsahuje .tif: " + url)
        member = tif_names[0]
        tmp = dest + ".part"
        with zf.open(member) as src, open(tmp, "wb") as out:
            shutil.copyfileobj(src, out)
    if os.path.getsize(tmp) < MIN_GRID_BYTES:
        os.remove(tmp)
        raise IOError("Rozbalený soubor je příliš malý: " + url)
    if os.path.exists(dest):
        os.remove(dest)
    os.rename(tmp, dest)


def missing_grids():
    missing = []
    if not _grid_ok(GRID_CZ):
        missing.append(GRID_CZ)
    if not _grid_ok(GRID_SK):
        missing.append(GRID_SK)
    return missing


def show_grid_install_guide(reason=None, iface=None):
    """Dialog s odkazy při chybějící mřížce nebo kolizi převodu."""
    global _GUIDE_SHOWN
    d = grids_dir()
    chyba = reason or "Mřížka chybí, je poškozená, nebo se převod s mřížkou nepodařil."
    text = (
        "<b>MoPSP — doinstalace transformačních mřížek (EPSG:5514 → 4326)</b><br/><br/>"
        + chyba + "<br/><br/>"
        "Plugin ukládá mřížky sem:<br/><code>" + d + "</code><br/><br/>"
        "<b>Česko (ČÚZK, table v1710, cca 3–4 cm)</b><br/>"
        "1. Stáhněte soubor "
        "<a href=\"" + URL_CZ_TIF + "\">" + GRID_CZ + "</a><br/>"
        "nebo ZIP z ČÚZK: <a href=\"" + URL_CZ_ZIP + "\">table_-y-x_3_v1710.tif.zip</a><br/>"
        "2. Rozbalený <code>.tif</code> vložte do složky výše.<br/>"
        "Info: <a href=\"" + URL_CZ_INFO + "\">geoportal.cuzk.cz</a><br/><br/>"
        "<b>Slovensko (UGKK, JTSK03, cca 5 cm)</b><br/>"
        "1. Stáhněte <a href=\"" + URL_SK_TIF + "\">" + GRID_SK + "</a><br/>"
        "2. Stejně ho vložte do téže složky.<br/>"
        "Info: <a href=\"" + URL_SK_PORTAL + "\">geoportal.sk — na stiahnutie</a><br/><br/>"
        "Katalog PROJ: <a href=\"" + URL_PROJ_CDN + "\">cdn.proj.org</a><br/><br/>"
        "Bez mřížky plugin použije jen celostátní 7parametrový Helmert (cca 1 m)."
    )
    box = QMessageBox()
    box.setWindowTitle("MoPSP — mřížky 5514 → 4326")
    box.setTextFormat(_RICH)
    box.setText(text)
    box.setStandardButtons(_BTN_OK)
    btn_cz = box.addButton("Otevřít odkaz ČR", _ROLE_ACTION)
    btn_sk = box.addButton("Otevřít odkaz SR", _ROLE_ACTION)
    btn_folder = box.addButton("Otevřít složku mřížek", _ROLE_ACTION)
    box.exec()
    clicked = box.clickedButton()
    try:
        if clicked == btn_cz:
            webbrowser.open(URL_CZ_TIF, new=0, autoraise=True)
        elif clicked == btn_sk:
            webbrowser.open(URL_SK_TIF, new=0, autoraise=True)
        elif clicked == btn_folder:
            webbrowser.open(d)
    except Exception:
        pass
    _GUIDE_SHOWN = True
    if iface is not None:
        try:
            iface.messageBar().pushMessage(
                "MoPSP",
                "Doinstalujte mřížky 5514→4326. Odkazy jsou v okně nápovědy.",
                duration=8)
        except Exception:
            pass


def ensure_jtsk_grids(iface=None, show_guide_on_fail=True):
    """Stáhne mřížky ČR a SR do složky pluginu a zaregistruje je v PROJ."""
    global _GRIDS_READY
    _register_proj_search_path()
    chyby = []
    kolize = []
    for name, urls in (
        (GRID_CZ, (URL_CZ_TIF, URL_CZ_ZIP)),
        (GRID_SK, (URL_SK_TIF,)),
    ):
        dest = _grid_path(name)
        if os.path.isfile(dest) and os.path.getsize(dest) < MIN_GRID_BYTES:
            kolize.append(name + " (soubor existuje, ale je poškozený / neúplný)")
            try:
                os.remove(dest)
            except Exception:
                pass
        if _grid_ok(name):
            continue
        try:
            _download_grid(name, urls)
        except Exception as e:
            chyby.append("%s: %s" % (name, e))
    _sync_grids_to_qgis_profile()
    _register_proj_search_path()
    _GRIDS_READY = not missing_grids()
    if (chyby or kolize) and show_guide_on_fail:
        duvody = []
        if kolize:
            duvody.append("Kolize / poškozený soubor: " + "; ".join(kolize) + ".")
        if chyby:
            duvody.append("Automatické stažení selhalo: " + "; ".join(chyby) + ".")
        show_grid_install_guide(" ".join(duvody), iface=iface)
    return _GRIDS_READY


def _country_from_layer(layer):
    """Hrubé rozlišení ČR / SR podle středu extentu v EPSG:5514."""
    if layer is None:
        return "CZ"
    try:
        ext = layer.extent()
        if ext is None or ext.isEmpty():
            return "CZ"
        cx = (ext.xMinimum() + ext.xMaximum()) / 2.0
        cy = (ext.yMinimum() + ext.yMaximum()) / 2.0
    except Exception:
        return "CZ"
    if cx > -360000:
        return "SK"
    if cx < -520000:
        return "CZ"
    if cy < -1230000:
        return "SK"
    return "CZ"


def operation_5514_to_4326(layer=None):
    """PROJ pipeline 5514 → 4326: mřížka země, jinak Helmert."""
    ensure_jtsk_grids(show_guide_on_fail=False)
    zeme = _country_from_layer(layer)
    if zeme == "SK" and _grid_ok(GRID_SK):
        return _sk_5514_to_4326_op()
    if zeme == "CZ" and _grid_ok(GRID_CZ):
        return _cz_5514_to_4326_op()
    if _grid_ok(GRID_CZ):
        return _cz_5514_to_4326_op()
    if _grid_ok(GRID_SK):
        return _sk_5514_to_4326_op()
    return HELMERT_5514_TO_4326


def operation_4326_to_5514(layer=None):
    ensure_jtsk_grids(show_guide_on_fail=False)
    zeme = _country_from_layer(layer)
    if zeme == "SK" and _grid_ok(GRID_SK):
        return _sk_4326_to_5514_op()
    if _grid_ok(GRID_CZ):
        return _cz_4326_to_5514_op()
    if _grid_ok(GRID_SK):
        return _sk_4326_to_5514_op()
    return HELMERT_4326_TO_5514


def reproject_layer_5514_to_4326(layer, output, iface=None):
    """native:reprojectlayer 5514→4326; při kolizi mřížky navede k doinstalaci a použije Helmert."""
    import processing
    from qgis.core import QgsCoordinateReferenceSystem
    ensure_jtsk_grids(show_guide_on_fail=False)
    params = {
        'INPUT': layer,
        'TARGET_CRS': QgsCoordinateReferenceSystem('EPSG:4326'),
        'CONVERT_CURVED_GEOMETRIES': False,
        'OPERATION': operation_5514_to_4326(layer),
        'OUTPUT': output}
    try:
        return processing.run("native:reprojectlayer", params)
    except Exception as e:
        if params['OPERATION'] != HELMERT_5514_TO_4326:
            show_grid_install_guide(
                "Kolize převodu s mřížkou (%s). Použiji záložní Helmert (cca 1 m)." % e,
                iface=iface)
            params['OPERATION'] = HELMERT_5514_TO_4326
            return processing.run("native:reprojectlayer", params)
        raise


def describe_transform_html():
    return (
        "<b>Převod vrstev EPSG:5514 → EPSG:4326</b><br/>"
        "1. Lokální mřížka (ČR: ČÚZK table v1710, SR: UGKK JTSK03) opraví deformace S-JTSK.<br/>"
        "2. Helmert převede S-JTSK/05 resp. JTSK03 na ETRS89, který se pro GNSS bere jako WGS 84.<br/>"
        "Přesnost s mřížkou: ČR cca 3–4 cm, SR cca 5 cm. Bez mřížky jen Helmert EPSG:1623 (cca 1 m)."
    )


def maybe_show_install_info():
    """Jednorázové instalační okno s popisem převodu po úspěšném stažení mřížek."""
    settings = QSettings("MoPSP", "Settings")
    if settings.value("grids_info_shown", defaultValue=0) in (1, "1"):
        return
    if missing_grids():
        return
    box = QMessageBox()
    box.setWindowTitle("MoPSP — instalace mřížek 5514 → 4326")
    box.setTextFormat(_RICH)
    box.setText(
        describe_transform_html() + "<br/><br/>"
        "Mřížky ČR i SR jsou nainstalovány ve složce:<br/><code>" + grids_dir() + "</code>"
    )
    box.setStandardButtons(_BTN_OK)
    box.exec()
    settings.setValue("grids_info_shown", 1)
