from __future__ import annotations
import logging
from typing import Any, Iterator, TYPE_CHECKING
from allytools.types import validate_cast
from zempy.zosapi.application.directories import Directories

from zempy.zosapi.enums.license_status import LicenseStatusType
from zempy.zosapi.system.adapters.optical_system import OpticalSystem
from zempy.zosapi.analysis.adapters.Message import Message
from zempy.dotnet.dotnet_system import DotNetSystem
from zempy.bridge import zemax_exceptions as _exc
from zempy.zosapi.system.protocols.IOpticalSystem import  IOpticalSystem

if TYPE_CHECKING:

    from zempy.zosapi.core import IZosapi

log = logging.getLogger(__name__)


class MessageLogSession:
    """Context manager around BeginMessageLogging / EndMessageLogging & retrieval."""
    def __init__(self, app: Any) -> None:
        self._app = app

    def __enter__(self) -> MessageLogSession:
        if not bool(self._app.BeginMessageLogging()):
            log.warning("BeginMessageLogging() returned False")
        return self

    def __exit__(self, exc_type, exc, tb) -> None:
        self._app.EndMessageLogging()

    def retrieve(self) -> str:
        """Retrieve buffered log messages."""
        return self._app.RetrieveLogMessages()


class ZOSApplication:
    """ Wrapper over ZOSAPI.IZOSAPI_Application"""

    __slots__ = ("_native_app", "_zosapi", "_closed")

    def __init__(self, native_app: Any, zosapi:IZosapi) -> None:
        self._native_app = native_app
        self._zosapi = zosapi
        self._closed = False


    def close(self) -> None:
        """Attempt to close OpticStudio; safe to call multiple times."""
        if self._closed:
            log.debug("ZOSApplication.close() skipped: already closed")
            return
        # In some cases the underlying COM/.NET object can already be released.
        close_fn = getattr(self._native_app, "CloseApplication", None)
        if not callable(close_fn):
            log.debug("CloseApplication not available on native app; skipping.")
            self._closed = True
            return
        log.debug("Closing OpticStudio application")
        try:
            close_fn()
            log.info("OpticStudio application closed.")
        except (OSError, RuntimeError) as e:
            log.warning("CloseApplication raised a runtime/OS error: %s", e)
        except Exception as e:
            if DotNetSystem.is_exception(e):
                log.warning("%s CLR exception: %s", e)
                raise _exc.ZemaxSystemError(f"Close application failed (CLR): {e}") from e
            raise
        finally:
            self._closed = True

    def __enter__(self) -> "ZOSApplication":
        return self

    def __exit__(self, exc_type, exc, tb) -> None:
        self.close()

    # ---------- Basic info ----------
    @property
    def is_closed(self) -> bool:
        return self._closed

    @property
    def is_valid_license(self) -> bool:
        return bool(self._native_app.IsValidLicenseForAPI)

    @property
    def license_status(self) -> LicenseStatusType:
        raw = self._native_app.LicenseStatus
        return LicenseStatusType.from_native(self._zosapi, raw)

    @property
    def initialization_errors(self) -> str:
        return str(self._native_app.InitializationErrors or "")

    @property
    def initialization_error_code(self) -> str:
        return str(self._native_app.InitializationErrorCode or "")

    @property
    def mode(self) -> Any:
        return self._native_app.Mode  # enum

    @property
    def serial(self) -> str:
        return str(self._native_app.SerialCode or "")

    @property
    def version(self) -> tuple[int, int, int]:
        return (int(self._native_app.ZOSMajorVersion),
                int(self._native_app.ZOSMinorVersion),
                int(self._native_app.ZOSSPVersion))

    @property
    def opticstudio_version_int(self) -> int:
        return int(self._native_app.OpticStudioVersion)

    @property
    def date_string(self) -> str:
        return str(self._native_app.GetDate())

    @property
    def paths(self) -> Directories:
        a = self._native_app
        return Directories(
            program=str(a.ProgramDir),
            lens=str(a.LensDir),
            objects=str(a.ObjectsDir),
            glass=str(a.GlassDir),
            zpl=str(a.ZPLDir),
            coating=str(a.CoatingDir),
            pop=str(a.POPDir),
            images=str(a.ImagesDir),
            solidworks=str(a.SolidWorksFilesDir),
            inventor=str(a.AutodeskInventorFilesDir),
            creo=str(a.CreoParametricFilesDir),
            matlab=str(a.MATLABFilesDir),
            scatter=str(a.ScatterDir),
            undo=str(a.UndoDir),
            samples=str(a.SamplesDir),
            data=str(a.ZemaxDataDir),
        )

    # ---------- System management (returns OpticalSystemAdapter) ----------
    @property
    def n_systems(self) -> int:
        return int(self._native_app.NumberOfOpticalSystems)

    @property
    def primary(self) -> IOpticalSystem:
        """Return the primary optical system, or raise if unavailable."""
        log.debug("Fetching PrimarySystem from native application.")
        native_sys = getattr(self._native_app, "PrimarySystem", None)
        if native_sys is None:
            raise RuntimeError("PrimarySystem is not available on the native application.")
        return validate_cast(OpticalSystem(self._zosapi, native_sys), IOpticalSystem)

    def systems(self) -> Iterator[IOpticalSystem]:
        """Iterate over all open systems, wrapped as OpticalSystemAdapter."""
        for i in range(self.n_systems):
            yield  validate_cast(OpticalSystem(self._native_app.GetSystemAt(i), self._zosapi), IOpticalSystem)

    def get_system(self, index: int) -> IOpticalSystem:
        if not (0 <= index < self.n_systems):
            raise IndexError(f"System index out of range: {index}")
        return validate_cast(OpticalSystem(self._native_app.GetSystemAt(index), self._zosapi), IOpticalSystem)

    def close_system(self, index: int, *, save_if_needed: bool = False) -> bool:
        return bool(self._native_app.CloseSystemAt(index, bool(save_if_needed)))

    def load_lens(self, lens_file: str) -> IOpticalSystem:
        """Creates a new optical system and loads the lens file."""
        log.info("Loading lens: %s", lens_file)
        native_sys = self._native_app.LoadNewSystem(lens_file)
        return validate_cast(OpticalSystem(native_sys, self._zosapi), IOpticalSystem)


    def create_system(self, system_type: Any) -> IOpticalSystem:
        """Creates a new empty optical system of the given SystemType."""
        native_sys = self._native_app.CreateNewSystem(system_type)
        return validate_cast(OpticalSystem(native_sys, self._zosapi), IOpticalSystem)

    def update_file_lists(self) -> None:
        self._native_app.UpdateFileLists()

    # ---------- Updates / preferences ----------
    def check_for_updates(self) -> None:
        self._native_app.CheckForUpdates()

    @property
    def check_updates_status(self) -> Any:
        return self._native_app.CheckForUpdatesStatus

    @property
    def check_updates_version(self) -> int:
        return int(self._native_app.CheckForUpdatesVersion)

    @property
    def check_updates_data(self) -> str:
        return str(self._native_app.CheckForUpdatesData or "")

    @property
    def preferences(self) -> Any:
        return self._native_app.Preferences

    # ---------- Operand / user analysis ----------
    @property
    def operand_results(self) -> Any:
        return self._native_app.OperandResults

    @property
    def operand_args(self) -> tuple[float, float, float, float]:
        a = self._native_app
        return (float(a.OperandArgument1), float(a.OperandArgument2),
                float(a.OperandArgument3), float(a.OperandArgument4))

    @property
    def user_analysis_data(self) -> Any:
        return self._native_app.UserAnalysisData

    @property
    def terminate_requested(self) -> bool:
        return bool(self._native_app.TerminateRequested)

    # ---------- UI progress (Plugin mode) ----------
    @property
    def show_changes_in_ui(self) -> bool:
        return bool(self._native_app.ShowChangesInUI)

    @show_changes_in_ui.setter
    def show_changes_in_ui(self, value: bool) -> None:
        self._native_app.ShowChangesInUI = bool(value)

    @property
    def progress_message(self) -> str:
        return str(self._native_app.ProgressMessage or "")

    @progress_message.setter
    def progress_message(self, msg: str) -> None:
        self._native_app.ProgressMessage = str(msg)

    @property
    def progress_percent(self) -> float:
        return float(self._native_app.ProgressPercent)

    @progress_percent.setter
    def progress_percent(self, pct: float) -> None:
        self._native_app.ProgressPercent = float(pct)

    # ---------- Message log ----------
    def clear_message_log(self) -> None:
        self._native_app.ClearMessageLog()

    def begin_message_logging(self) -> bool:
        return bool(self._native_app.BeginMessageLogging())

    def end_message_logging(self) -> bool:
        return bool(self._native_app.EndMessageLogging())

    def retrieve_log_messages(self) -> str:
        return self._native_app.RetrieveLogMessages()

    def message_log(self) -> MessageLogSession:
        return MessageLogSession(self._native_app)

    # ---------- Data files / settings ----------
    def create_settings(self) -> Any:
        return self._native_app.CreateSettings()

    def create_settings_from(self, parent: Any) -> Any:
        return self._native_app.CreateSettingsFromParent(parent)

    def copy_settings(self, src: Any, dst: Any) -> bool:
        return bool(self._native_app.CopySettingsData(src, dst))

    def open_data_file(self, filename: str) -> Any:
        return self._native_app.OpenDataFile(filename)

    def create_data_file(self, filename: str, file_type: Any, data1: int, data2: int) -> Any:
        return self._native_app.CreateDataFile(filename, file_type, int(data1), int(data2))

    # ---------- Callbacks (wrap IMessage -> ZOSMessage) ----------
    def load_c_callback(self, c_lib: str, callback_name: str, settings: Any) -> Any:
        return self._native_app.LoadCCallback(c_lib, callback_name, settings)

    def register_c_operand_callback(self, c_lib: str, callback_name: str, settings: Any) -> Message:
        msg = self._native_app.RegisterCOperandCallback(c_lib, callback_name, settings)
        return Message.from_native(msg)

    def load_net_callback(self, assembly: str, type_name: str, settings: Any) -> Any:
        return self._native_app.LoadNETCallback(assembly, type_name, settings)

    def register_net_operand_callback(self, assembly: str, type_name: str, settings: Any) -> Message:
        msg = self._native_app.RegisterNETOperandCallback(assembly, type_name, settings)
        return Message.from_native(msg)

    # ---------- Advanced/diagnostic ----------
    def run_command(self, command: str) -> str:
        return str(self._native_app.RunCommand(command))


    #TODO should not pass zosapi here.
    @classmethod
    def from_connection(cls, conn: Any, zosapi:IZosapi,  *, create_new: bool = False, show_ui: bool = True) -> "ZOSApplication":
        """
        Build from an IZOSAPI_Connection.
          - If create_new=True: conn.CreateNewApplication(show_ui)
          - Else:               conn.ConnectToApplication()
        """
        app = conn.CreateNewApplication(show_ui) if create_new else conn.ConnectToApplication()
        if app is None:
            raise RuntimeError("Failed to acquire IZOSAPI_Application from connection.")
        return cls(app, zosapi)

    @property
    def raw(self) -> Any:
        """Underlying IZOSAPI_Application."""
        return self._native_app

    def __str__(self) -> str:
        """Human-readable summary for logging and debugging."""
        try:
            major, minor, sp = self.version
            license_status = self.license_status.name if hasattr(self.license_status, "name") else str(
                self.license_status)
            return (
                f"<ZOSApplication "
                f"serial={self.serial or '-'} "
                f"version={major}.{minor}.{sp} "
                f"license={license_status} "
                f"mode={getattr(self.mode, 'name', self.mode)} "
                f"systems={self.n_systems} "
                f"closed={self._closed}>"
            )
        except Exception as e:
            # graceful fallback if anything fails (e.g. disconnected session)
            return f"<ZOSApplication (unavailable: {e!r})>"