from pathlib import Path
from typing import Any
from urllib.request import urlretrieve

import numpy as np
import numpy.typing as npt
import tetra3  # type: ignore
from pydantic import BaseModel, Field

from pixelemon.constants import ZERO
from pixelemon.logging import PixeLemonLog

_GH_URL = "https://github.com/citra-space/pixelemon/releases/download/general-purpose-database"
TETRA_DB_NAME = "tyc_db_to_40_deg.npz"
TETRA_DATABASE_URL = f"{_GH_URL}/{TETRA_DB_NAME}"
TETRA_DATABASE_PATH = Path(__file__).parent / TETRA_DB_NAME


class TetraSolver(tetra3.Tetra3):

    _instance: "TetraSolver | None" = None
    _initialized: bool = False

    def __new__(cls, *args, **kwargs) -> "TetraSolver":
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

    def __init__(self, database_path: Path | None = None, reload: bool = False):
        if not self._initialized or reload:
            if database_path is None:
                super().__init__()
            else:
                super().__init__(load_database=database_path.as_posix())
            self.settings = TetraSettings.model_validate(self.database_properties)
            self._initialized = True

    @classmethod
    def low_memory(cls) -> "TetraSolver":
        solver = cls(reload=True)
        return solver

    @classmethod
    def high_memory(cls) -> "TetraSolver":
        if not TETRA_DATABASE_PATH.exists():
            PixeLemonLog().warning("Tetra database not found locally.  One-time download may take several minutes.")
            PixeLemonLog().info(f"Downloading Tetra database from {_GH_URL} to {TETRA_DATABASE_PATH}")
            urlretrieve(TETRA_DATABASE_URL, TETRA_DATABASE_PATH.as_posix())
        solver = cls(TETRA_DATABASE_PATH, reload=True)
        return solver

    def solve_from_centroids(
        self,
        star_centroids: npt.NDArray[np.float32],
        img_size: tuple[int, int],
        fov: float,
    ) -> "PlateSolve | None":
        try:
            result = super().solve_from_centroids(star_centroids, size=img_size, fov_estimate=fov, return_matches=True)
            solve = PlateSolve(result)
            PixeLemonLog().info(f"Completed plate solve in {solve.solve_time:.2f} ms")
        except ValueError as e:
            PixeLemonLog().error(e)
            return None
        return solve


class TetraSettings(BaseModel):
    dimmest_star_magnitude: float = Field(
        ...,
        description="The dimmest star magnitude to consider in the plate solve",
        alias="star_max_magnitude",
    )
    max_field_of_view: float = Field(
        ...,
        description="The maximum field of view in degrees the solver can reliably handle",
        alias="max_fov",
    )
    min_field_of_view: float = Field(
        ...,
        description="The minimum field of view in degrees the solver can reliably handle",
        alias="min_fov",
    )
    verification_star_count: int = Field(
        ...,
        description="The number of stars to use for verification of the plate solve",
        alias="verification_stars_per_fov",
    )

    def model_post_init(self, _: Any) -> None:
        PixeLemonLog().info(f"Tetra dimmest star magnitude set to {self.dimmest_star_magnitude}")
        PixeLemonLog().info(f"Tetra max field of view set to {self.max_field_of_view} degrees")
        PixeLemonLog().info(f"Tetra min field of view set to {self.min_field_of_view} degrees")
        PixeLemonLog().info(f"Tetra verification star count set to {self.verification_star_count}")


class PlateSolve:

    def __init__(self, tetra_output):
        if tetra_output is None:
            raise ValueError("Plate solve failed")
        elif tetra_output["Prob"] > ZERO:
            raise ValueError("Plate solve returned false positive probability greater than zero")

        self.right_ascension = tetra_output["RA"]
        self.declination = tetra_output["Dec"]
        self.roll = tetra_output["Roll"]
        self.estimated_horizontal_fov = tetra_output["FOV"]
        self.root_mean_square_error = tetra_output["RMSE"]
        self.number_of_stars = tetra_output["Matches"]
        self.false_positive_probability = tetra_output["Prob"]
        self.solve_time = tetra_output["T_solve"]
        self.matched_stars = np.array(tetra_output["matched_stars"])
        self.matched_centroids = np.array(tetra_output["matched_centroids"])
        self.distortion = tetra_output["distortion"]
