# Copyright © 2024, UChicago Argonne, LLC
# BSD OPEN SOURCE LICENSE. Full license can be found in LICENSE.md
import logging
import os
import shutil
import urllib
import urllib.request
from os.path import join, basename
from pathlib import Path
from sqlite3 import Connection
from tempfile import gettempdir
from typing import Optional
from zipfile import ZipFile

from polaris.utils.dir_utils import mkdir_p
from polaris.utils.env_utils import is_not_windows, is_windows
from polaris.utils.environment import inside_qgis
from polaris.utils.type_utils import AnyPath


def connect_spatialite(path_to_file: AnyPath, missing_ok: bool = False) -> Connection:
    if inside_qgis:
        import qgis  # type: ignore

        return qgis.utils.spatialite_connect(str(path_to_file))

    ensure_spatialite_binaries()
    return _connect_spatialite(path_to_file, missing_ok)


def _connect_spatialite(path_to_file: AnyPath, missing_ok: bool = False):
    from polaris.utils.database.db_utils import safe_connect

    conn = safe_connect(path_to_file, missing_ok)
    conn.enable_load_extension(True)

    # Load the version specified in the environment (if specified) or just attempt to find on the path otherwise
    conn.load_extension(os.environ.get("MOD_SPATIALITE_PATH", "mod_spatialite"))
    return conn


def is_spatialite(conn):
    from polaris.utils.database.db_utils import has_table

    return has_table(conn, "geometry_columns")


def ensure_spatialite_binaries(directory: Optional[AnyPath] = None) -> None:
    if is_windows():
        return ensure_spatialite_binaries_windows(directory)
    else:
        return ensure_spatialite_binaries_ubuntu()


def ensure_spatialite_binaries_ubuntu() -> None:
    try:
        conn = _connect_spatialite(":memory:")
        conn.close()
        return

    except Exception:
        pass

    if not Path("/usr/local/lib/mod_spatialite.so").exists():
        raise Exception("I don't know where to find mod_spatialite I'm sorry")

    # We can't modify LD_LIBRARY_PATH dynamically, so we just use the full path to the so file
    os.environ["MOD_SPATIALITE_PATH"] = "/usr/local/lib/mod_spatialite"


def ensure_spatialite_binaries_windows(directory: Optional[AnyPath] = None, raise_error=True) -> None:
    dir_path = directory or gettempdir()

    if not _dll_already_exists(Path(dir_path)):
        _download_and_extract_spatialite(Path(dir_path))

    dir_path = str(dir_path)
    if dir_path not in os.environ["PATH"] or "PROJ_LIB" not in os.environ:
        os.environ["PATH"] = dir_path + os.pathsep + os.environ["PATH"]
        os.environ["PROJ_LIB"] = dir_path

    # We need to have the proj.db file in place.
    # The easiest one on Windows is in the public user. On Linux it should not be necessary
    # See why: https://www.gaia-gis.it/fossil/libspatialite/wiki?name=PROJ.6
    projdb_dir = "C:/Users/Public/spatialite/proj"
    Path(projdb_dir).mkdir(parents=True, exist_ok=True)
    if os.path.isfile(join(projdb_dir, "proj.db")):
        return

    shutil.copyfile(join(dir_path, "proj.db"), join(projdb_dir, "proj.db"))


def _dll_already_exists(d: Path) -> bool:
    ext = "so" if is_not_windows() else "dll"
    return (d / f"mod_spatialite.{ext}").exists()


def _download_and_extract_spatialite(directory: AnyPath) -> None:
    mkdir_p(directory)

    # Pretend to be Firefox so we don't get bot-blocked
    opener = urllib.request.build_opener()
    opener.addheaders = [("User-agent", "Mozilla/5.0")]
    urllib.request.install_opener(opener)

    url = "http://polaris.taps.anl.gov/resources/spatialite/mod_spatialite-5.1.0-win-amd64.zip"
    zip_file = join(directory, basename(url))

    urllib.request.urlretrieve(url, zip_file)
    ZipFile(zip_file).extractall(directory)
    os.remove(zip_file)


def spatialize_db(conn, logger=None):
    logger = logger or logging.getLogger("polaris")
    logger.info("Adding Spatialite infrastructure to the database")
    curr = conn.cursor()
    if not inside_qgis and not is_spatialite(conn):
        try:
            curr.execute("SELECT InitSpatialMetaData();")
            conn.commit()
        except Exception as e:
            logger.error("Problem with spatialite", e.args)
            raise e
    if not is_spatialite(conn):
        raise RuntimeError("Something went wrong")


def get_spatialite_version(conn):
    res = conn.execute("SELECT spatialite_version()").fetchone()
    return None if res is None else res[0]
