# General Server Utilities
#
# Copyright (C) 2023 Eric Callahan <arksine.code@gmail.com>
#
# This file may be distributed under the terms of the GNU GPLv3 license

from __future__ import annotations
import importlib.resources as ilr
import pathlib
import sys
import site
import re
import json
import logging
from dataclasses import dataclass
from importlib_metadata import Distribution, PathDistribution, PackageMetadata
from .exceptions import ServerError

# Annotation imports
from typing import (
    Optional,
    Dict,
    Any
)

def package_path() -> pathlib.Path:
    return pathlib.Path(__file__).parent.parent

def source_path() -> pathlib.Path:
    return package_path().parent

def is_git_repo(src_path: Optional[pathlib.Path] = None) -> bool:
    if src_path is None:
        src_path = source_path()
    return src_path.joinpath(".git").is_dir()

def find_git_repo(src_path: Optional[pathlib.Path] = None) -> Optional[pathlib.Path]:
    if src_path is None:
        src_path = source_path()
    if src_path.joinpath(".git").is_dir():
        return src_path
    for parent in src_path.parents:
        if parent.joinpath(".git").is_dir():
            return parent
    return None

def is_dist_package(item_path: Optional[pathlib.Path] = None) -> bool:
    """
    Check if the supplied path exists within a python dist installation or
    site installation.
    """
    if item_path is None:
        # Check Moonraker's package path
        item_path = package_path()
        if hasattr(site, "getsitepackages"):
            # The site module is present, get site packages for Moonraker's venv.
            # This is more "correct" than the fallback method.
            for site_dir in site.getsitepackages():
                site_path = pathlib.Path(site_dir)
                try:
                    if site_path.samefile(item_path.parent):
                        return True
                except Exception:
                    pass
    # Make an assumption based on the item and/or its parents.  If a folder
    # is named site-packages or dist-packages then presumably it is an
    # installed package
    if item_path.name in ("dist-packages", "site-packages"):
        return True
    for parent in item_path.parents:
        if parent.name in ("dist-packages", "site-packages"):
            return True
    return False

def package_version() -> Optional[str]:
    try:
        import moonraker.__version__ as ver  # type: ignore
        version = ver.__version__
    except Exception:
        pass
    else:
        if version:
            return version
    return None

def read_asset(asset_name: str) -> Optional[str]:
    if sys.version_info < (3, 10):
        with ilr.path("moonraker.assets", asset_name) as p:
            if not p.is_file():
                return None
            return p.read_text()
    else:
        files = ilr.files("moonraker.assets")
        with ilr.as_file(files.joinpath(asset_name)) as p:
            if not p.is_file():
                return None
            return p.read_text()

def get_asset_path() -> Optional[pathlib.Path]:
    if sys.version_info < (3, 10):
        with ilr.path("moonraker.assets", "__init__.py") as p:
            asset_path = p.parent
    else:
        files = ilr.files("moonraker.assets")
        with ilr.as_file(files.joinpath("__init__.py")) as p:
            asset_path = p.parent
    if not asset_path.is_dir():
        # Somehow running in a zipapp.  This is an error.
        return None
    return asset_path

def _load_release_info_json(dist_info: Distribution) -> Optional[Dict[str, Any]]:
    files = dist_info.files
    if files is None:
        return None
    for dist_file in files:
        if dist_file.parts[0] in ["..", "/"]:
            continue
        if dist_file.name == "release_info":
            pkg = dist_file.parts[0]
            logging.info(f"Package {pkg}: Detected release_info json file")
            try:
                return json.loads(dist_file.read_text())
            except Exception:
                logging.exception(f"Failed to load release_info from {dist_file}")
    return None

def _load_direct_url_json(dist_info: Distribution) -> Optional[Dict[str, Any]]:
    ret: Optional[str] = dist_info.read_text("direct_url.json")
    if ret is None:
        return None
    try:
        direct_url: Dict[str, Any] = json.loads(ret)
    except json.JSONDecodeError:
        return None
    return direct_url

def normalize_project_name(name: str) -> str:
    return re.sub(r"[-_.]+", "-", name).lower().replace('-', '_')

def load_distribution_info(
    venv_path: pathlib.Path, project_name: str
) -> PackageInfo:
    proj_name_normalized = normalize_project_name(project_name)
    site_items = venv_path.joinpath("lib").glob("python*/site-packages/")
    lib_paths = [str(p) for p in site_items if p.is_dir()]
    for dist_info in Distribution.discover(name=project_name, path=lib_paths):
        metadata = dist_info.metadata
        if metadata is None:
            continue
        if not isinstance(dist_info, PathDistribution):
            logging.info(f"Project {dist_info.name} not a PathDistribution")
            continue
        metaname = normalize_project_name(metadata["Name"] or "")
        if metaname != proj_name_normalized:
            continue
        release_info = _load_release_info_json(dist_info)
        install_info = _load_direct_url_json(dist_info)
        return PackageInfo(
            dist_info, metadata, release_info, install_info
        )
    raise ServerError(f"Failed to find distribution info for project {project_name}")

def is_vitualenv_project(
    venv_path: Optional[pathlib.Path] = None,
    pkg_path: Optional[pathlib.Path] = None,
    project_name: str = "moonraker"
) -> bool:
    if venv_path is None:
        venv_path = pathlib.Path(sys.exec_prefix)
    if pkg_path is None:
        pkg_path = package_path()
    if not pkg_path.exists():
        return False
    try:
        pkg_info = load_distribution_info(venv_path, project_name)
    except Exception:
        return False
    site_path = pathlib.Path(str(pkg_info.dist_info.locate_file("")))
    for parent in pkg_path.parents:
        try:
            if site_path.samefile(parent):
                return True
        except Exception:
            pass
    return True

@dataclass(frozen=True)
class PackageInfo:
    dist_info: Distribution
    metadata: PackageMetadata
    release_info: Optional[Dict[str, Any]]
    direct_url_data: Optional[Dict[str, Any]]