"""Main API class for the SQ API."""

import ctypes
import os
import asyncio
import time
import logging
import threading
from typing import Tuple, Optional, Any
from ctypes import c_int, c_double, c_char_p, c_void_p, POINTER

from .constants import cmaxphases, LIB_NAME, SQErrorCode, SaveTaskFlags
from .types import (
    TOpenTaskRec, TLoadPatternRec, TStartRefinementRec,
    TSearchMatchCondition, TReportRec,
    TSetTraceRec, TGetTraceRec, TGlobalsRec,
    ASearchMatchRec, PASearchMatchRec, AResultRec, PAResultRec,
    ATrace, PATrace, APhaseRefinablesRec, PAPhaseRefinablesRec,
    PACyrstalliteRec, PAAmorphous, PGlobalsRec
)
from .exceptions import SQAPIError, SQAPITaskError, SQAPIRefinementError
from .dataclasses import (
    TaskInfo, PatternInfo, RefinementParams, RefinementResult
)
from .config import SQAPIConfig
from .utils import (
    validate_task_file, validate_pattern_file, validate_job_handle,
    validate_lib_dir, format_error_message
)

# Module-level logger
logger = logging.getLogger(__name__)

# Thread safety: RLock for all DLL operations
_dll_lock = threading.RLock()


def _configure_logger(log_level: int) -> None:
    """Configure module logger if not already configured."""
    logger.setLevel(log_level)
    # Only add handler if logger has no handlers and isn't propagating to a configured parent
    if not logger.handlers and not (logger.parent and logger.parent.handlers):
        handler = logging.StreamHandler()
        handler.setLevel(log_level)
        handler.setFormatter(logging.Formatter('%(name)s - %(levelname)s - %(message)s'))
        logger.addHandler(handler)


class TaskContext:
    """Context manager for task operations with automatic cleanup."""
    
    __slots__ = ('api', 'filename', 'use_unicode', 'job_handle', 'task_info')
    
    def __init__(self, api: 'SQAPI', filename: str, use_unicode: bool = True) -> None:
        """Initialize the task context manager."""
        self.api = api
        self.filename = filename
        self.use_unicode = use_unicode
        self.job_handle: Optional[int] = None
        self.task_info: Optional[TaskInfo] = None
    
    def __enter__(self) -> Tuple[int, TaskInfo]:
        """Open task and return job handle and task info."""
        logger.info(f"Opening task context for file: {self.filename}")
        self.task_info = self.api.open_task(self.filename, self.use_unicode)
        self.job_handle = self.task_info.job_handle
        return self.job_handle, self.task_info
    
    def __exit__(self, exc_type: Optional[type], exc_val: Optional[Exception], exc_tb: Optional[Any]) -> bool:
        """Close task on context exit."""
        if self.job_handle is not None:
            try:
                self.api.close_task(self.job_handle)
            except Exception as e:
                logger.warning(f"Error closing task {self.job_handle}: {e}")
        return False


class SQAPI:
    """Main API class for interacting with the Siroquant SQ4 DLL.
    
    Thread-safe: All DLL operations are protected by a module-level RLock.
    """
    
    __slots__ = ('config', 'lib', '_initialized', '_lib_dir_set')
    
    def __init__(self, config: Optional[SQAPIConfig] = None, log_level: int = logging.WARNING) -> None:
        """Initialize the SQ API instance.
        
        Args:
            config: Configuration object. If None, uses defaults.
            log_level: Logging level (default: WARNING).
        """
        _configure_logger(log_level)
        logger.info(f"Initializing SQAPI")
        
        self.config = config or SQAPIConfig(validate=False)
        
        # Find DLL from config
        package_dir = os.path.dirname(os.path.dirname(__file__))
        dll_path = self.config.find_dll(package_dir)
        logger.debug(f"Using DLL path: {dll_path}")
        
        try:
            self.lib = ctypes.WinDLL(dll_path)
            logger.debug(f"DLL loaded successfully")
        except OSError as e:
            raise SQAPIError(f"Failed to load DLL: {dll_path}. Error: {e}")
        
        self._setup_prototypes()
        self._initialized = False
        self._lib_dir_set = False
    
    def _setup_prototypes(self) -> None:
        """Setup function prototypes for the DLL."""
        logger.debug("Setting up DLL function prototypes")
        
        self.lib.SQ4Initialize.argtypes = []
        self.lib.SQ4Initialize.restype = c_int

        self.lib.SQ4LihDir2.argtypes = [c_void_p]
        self.lib.SQ4LihDir2.restype = c_int

        self.lib.SQ4OpenTask1.argtypes = [c_char_p, POINTER(TOpenTaskRec)]
        self.lib.SQ4OpenTask1.restype = c_int

        self.lib.SQ4OpenTask2.argtypes = [c_void_p, POINTER(TOpenTaskRec)]
        self.lib.SQ4OpenTask2.restype = c_int

        self.lib.SQ4LoadPattern1.argtypes = [c_int, c_char_p, POINTER(TLoadPatternRec)]
        self.lib.SQ4LoadPattern1.restype = c_int

        try:
            self.lib.SQ4LoadPattern2.argtypes = [c_int, c_void_p, POINTER(TLoadPatternRec)]
            self.lib.SQ4LoadPattern2.restype = c_int
        except AttributeError:
            pass

        self.lib.SQ4SetPattern1.argtypes = [c_int, PATrace, c_int, c_char_p, TSetTraceRec]
        self.lib.SQ4SetPattern1.restype = c_int

        self.lib.SQ4StartRefinement.argtypes = [c_int, POINTER(TStartRefinementRec)]
        self.lib.SQ4StartRefinement.restype = c_int

        self.lib.SQ4SearchMatch.argtypes = [c_int, TSearchMatchCondition, POINTER(PASearchMatchRec)]
        self.lib.SQ4SearchMatch.restype = c_int

        self.lib.SQ4Eliminate.argtypes = [c_int, c_int, TSearchMatchCondition, POINTER(PASearchMatchRec)]
        self.lib.SQ4Eliminate.restype = c_int

        self.lib.SQ4CheckRefinement.argtypes = [c_int]
        self.lib.SQ4CheckRefinement.restype = c_int

        self.lib.SQ4ReportResultsFull.argtypes = [
            c_int, POINTER(TReportRec), PAResultRec, c_int, POINTER(TGlobalsRec), PAPhaseRefinablesRec
        ]
        self.lib.SQ4ReportResultsFull.restype = c_int

        self.lib.SQ4ReportResults.argtypes = [c_int, POINTER(TReportRec), PAResultRec, c_int]
        self.lib.SQ4ReportResults.restype = c_int

        self.lib.SQ4CrystalliteSize.argtypes = [c_int, c_double, c_int, c_int, PACyrstalliteRec]
        self.lib.SQ4CrystalliteSize.restype = c_int

        self.lib.SQ4AmorphousContent.argtypes = [c_int, c_double, c_int, c_int, PAAmorphous]
        self.lib.SQ4AmorphousContent.restype = c_int

        self.lib.SQ4LoadTrace.argtypes = [c_int, PATrace, POINTER(c_int), c_int, POINTER(TGetTraceRec)]
        self.lib.SQ4LoadTrace.restype = c_int

        self.lib.SQ4SaveTask1.argtypes = [c_int, c_char_p, c_int]
        self.lib.SQ4SaveTask1.restype = c_int

        self.lib.SQ4SaveTask2.argtypes = [c_int, c_void_p, c_int]
        self.lib.SQ4SaveTask2.restype = c_int

        self.lib.SQ4CloseTask.argtypes = [c_int]
        self.lib.SQ4CloseTask.restype = c_int

        self.lib.SQ4Finalize.argtypes = []
        self.lib.SQ4Finalize.restype = c_int

    def initialize(self) -> None:
        """Initialize the SQ API. Must be called before any operations."""
        if not self._lib_dir_set and self.config.lib_dir:
            self.set_lib_dir(self.config.get_lib_dir())
        
        logger.info("Calling SQ4Initialize()")
        with _dll_lock:
            result = self.lib.SQ4Initialize()
        
        if result in (SQErrorCode.INITIALIZED, SQErrorCode.OK):
            self._initialized = True
            logger.info("API initialized successfully")
        elif result < 0:
            logger.error(f"API initialization failed with error code: {result}")
            error_msg = self._get_init_error_message(result)
            raise SQAPIError(error_msg, error_code=result)
    
    def _get_init_error_message(self, result: int) -> str:
        """Get detailed error message for initialization failures."""
        if result == SQErrorCode.FINALIZATION_FAILURE:
            return (
                "API initialization failed: Previous finalization did not complete successfully.\n"
                "Please restart the application."
            )
        elif result == SQErrorCode.DONGL_FAILURE:
            from .constants import ERROR_MESSAGES
            return ERROR_MESSAGES.get(result, f"Unknown error: {result}")
        elif result == SQErrorCode.UNKNOWN_EXCEPTION:
            return "API initialization failed: An unexpected exception occurred in the DLL."
        else:
            return format_error_message(result, "operation: initialize")

    def set_lib_dir(self, path: str) -> None:
        """Set the library directory for phase database files."""
        abs_path = validate_lib_dir(path)
        logger.debug(f"Setting library directory: {abs_path}")
        
        with _dll_lock:
            result = self.lib.SQ4LihDir2(ctypes.c_wchar_p(abs_path))
        
        if result == SQErrorCode.OK:
            self._lib_dir_set = True
        elif result < 0:
            raise SQAPIError(format_error_message(result, f"path: {abs_path}"), error_code=result)

    def open_task(self, filename: str, use_unicode: bool = True) -> TaskInfo:
        """Open a Siroquant task file (.tsk or .tmk).
        
        Args:
            filename: Task file path (relative or absolute).
            use_unicode: Whether to use Unicode API (default: True).
            
        Returns:
            TaskInfo object containing task information.
        """
        abs_filename = validate_task_file(filename)
        logger.info(f"Opening task file: {abs_filename}")
        
        open_rec = TOpenTaskRec()
        with _dll_lock:
            if use_unicode:
                job_handle = self.lib.SQ4OpenTask2(ctypes.c_wchar_p(abs_filename), ctypes.byref(open_rec))
            else:
                job_handle = self.lib.SQ4OpenTask1(
                    ctypes.c_char_p(abs_filename.encode('mbcs')), 
                    ctypes.byref(open_rec)
                )
        
        if open_rec.return_val < 0:
            error_msg = format_error_message(open_rec.return_val, f"filename: {abs_filename}")
            raise SQAPITaskError(error_msg, error_code=open_rec.return_val)
        
        task_info = TaskInfo.from_ctypes(job_handle, open_rec)
        logger.info(f"Task opened: {task_info}")
        return task_info

    def load_pattern(self, n: int, filename: str, use_unicode: bool = True) -> PatternInfo:
        """Load an X-ray diffraction pattern file into a task.
        
        Args:
            n: Job handle.
            filename: Pattern file path (relative or absolute).
            use_unicode: Whether to use Unicode API (default: True).
            
        Returns:
            PatternInfo object containing pattern information.
        """
        validate_job_handle(n)
        abs_filename = validate_pattern_file(filename)
        logger.info(f"Loading pattern: {abs_filename} for job {n}")
        
        load_rec = TLoadPatternRec()
        with _dll_lock:
            if use_unicode and hasattr(self.lib, 'SQ4LoadPattern2'):
                res = self.lib.SQ4LoadPattern2(n, ctypes.c_wchar_p(abs_filename), ctypes.byref(load_rec))
            else:
                res = self.lib.SQ4LoadPattern1(
                    n, 
                    ctypes.c_char_p(abs_filename.encode('mbcs')), 
                    ctypes.byref(load_rec)
                )
        
        pattern_info = PatternInfo.from_ctypes(load_rec)
        
        if res != SQErrorCode.OK or pattern_info.return_code < 0:
            error_code = pattern_info.return_code if pattern_info.return_code < 0 else res
            raise SQAPIError(format_error_message(error_code, f"filename: {filename}"), error_code=error_code)
        
        logger.info(f"Pattern loaded: {pattern_info}")
        return pattern_info

    def start_refinement(self, n: int, params: Optional[RefinementParams] = None) -> None:
        """Start a quantitative phase analysis refinement.
        
        Args:
            n: Job handle from open_task().
            params: Refinement parameters. If None, a zeroed record is passed to
                the DLL which triggers the "run as is" branch — the task's stored
                configuration (AutoPreScale, stages, etc.) is used unchanged.
        
        Raises:
            SQAPIRefinementError: If refinement cannot be started.
        """
        validate_job_handle(n)
        logger.info(f"Starting refinement for job {n}")
        
        if params is not None:
            start_rec = params.to_ctypes()
        else:
            start_rec = TStartRefinementRec()  # zero-initialised → DLL "run as is" branch
        with _dll_lock:
            result = self.lib.SQ4StartRefinement(n, ctypes.byref(start_rec))
        
        if params is not None:
            params.ret_flags = start_rec.RetFlags
        
        if result != SQErrorCode.OK:
            raise SQAPIRefinementError(
                format_error_message(result, f"job_handle: {n}"), 
                error_code=result
            )

    def check_refinement(self, n: int) -> int:
        """Check the status of a running refinement.
        
        Args:
            n: Job handle.
        
        Returns:
            Status code (TASK_COMPLETED, TASK_REFINING, REFINEMENT_FAILED, etc.)
        """
        validate_job_handle(n)
        with _dll_lock:
            status = self.lib.SQ4CheckRefinement(n)
        logger.debug(f"Refinement status for job {n}: {status}")
        return status

    def _poll_refinement(self, job_handle: int, timeout: float, check_interval: float) -> int:
        """Internal polling loop for refinement completion.
        
        Args:
            job_handle: Job handle.
            timeout: Maximum wait time in seconds.
            check_interval: Time between status checks in seconds.
        
        Returns:
            Final refinement status code.
        
        Raises:
            TimeoutError: If refinement doesn't complete within timeout.
        """
        start_time = time.time()
        while time.time() - start_time < timeout:
            status = self.check_refinement(job_handle)
            
            if status == SQErrorCode.TASK_COMPLETED:
                logger.info("Refinement completed successfully")
                return status
            elif status == SQErrorCode.REFINEMENT_FAILED:
                logger.warning("Refinement failed")
                return status
            elif status not in (SQErrorCode.TASK_REFINING, SQErrorCode.TASK_NOT_STARTED):
                # Other error codes
                return status
            
            time.sleep(check_interval)
        
        raise TimeoutError(f"Refinement did not complete within {timeout} seconds")

    def report_results(self, n: int) -> RefinementResult:
        """Get the results from a completed refinement.
        
        Args:
            n: Job handle.
        
        Returns:
            RefinementResult object with chi-squared, R-factor, and phase results.
        """
        validate_job_handle(n)
        logger.info(f"Retrieving results for job {n}")
        
        report_rec = TReportRec()
        phases = AResultRec()
        
        with _dll_lock:
            res = self.lib.SQ4ReportResults(n, ctypes.byref(report_rec), ctypes.pointer(phases), cmaxphases)
        
        if res < 0:
            raise SQAPIError(format_error_message(res, f"job_handle: {n}"), error_code=res)
        
        result = RefinementResult.from_ctypes(report_rec, phases, report_rec.nPhases)
        logger.info(f"Results: {result}")
        return result

    def close_task(self, n: int) -> None:
        """Close a task and free associated resources.
        
        Args:
            n: Job handle.
        """
        validate_job_handle(n)
        logger.info(f"Closing task {n}")
        
        with _dll_lock:
            result = self.lib.SQ4CloseTask(n)
        
        if result != SQErrorCode.OK:
            raise SQAPITaskError(format_error_message(result, f"job_handle: {n}"), error_code=result)

    def save_task(
        self,
        n: int,
        filename: str,
        flags: int = SaveTaskFlags.DEFAULT,
        use_unicode: bool = True,
    ) -> None:
        """Save a task to disk.

        Persists the current state of the task (phases, parameters, and
        optionally refinement results) to a ``.tsk`` or ``.tmk`` file.

        The task must not be actively refining when this is called.

        Args:
            n: Job handle from open_task().
            filename: Destination file path.  Relative paths are resolved
                to absolute paths before being passed to the DLL.
            flags: Bitfield of :class:`SaveTaskFlags` values controlling save
                behaviour.  Defaults to ``SaveTaskFlags.DEFAULT`` (0).

                Common combinations:

                * ``SaveTaskFlags.DEFAULT`` – overwrite original file,
                  current format.
                * ``SaveTaskFlags.ENFORCE_DIRECTORIES`` – create missing
                  output directories.
                * ``SaveTaskFlags.AS_TSK`` – force ``.tsk`` format.
                * ``SaveTaskFlags.AS_TMK`` – force ``.tmk`` template format.
                * ``SaveTaskFlags.TRACSCAL`` – embed TRACSCAL data.

            use_unicode: Use the Unicode API (``SQ4SaveTask2``). Set to
                ``False`` to use the ANSI API (``SQ4SaveTask1``).
                Defaults to ``True``.

        Raises:
            ValueError: If the job handle is invalid.
            SQAPITaskError: If the task is still refining or the save fails.

        Example:
            >>> api.save_task(task_info.job_handle, r"output\\result.tsk")

            >>> # Save to a new path and create any missing directories
            >>> from pysq import SaveTaskFlags
            >>> api.save_task(
            ...     task_info.job_handle,
            ...     r"output\\subdir\\result.tsk",
            ...     SaveTaskFlags.ENFORCE_DIRECTORIES,
            ... )

            >>> # Save as a template file with TRACSCAL data embedded
            >>> api.save_task(
            ...     task_info.job_handle,
            ...     "template.tmk",
            ...     SaveTaskFlags.AS_TMK | SaveTaskFlags.TRACSCAL,
            ... )
        """
        validate_job_handle(n)

        # IMPORTANT: the DLL calls SetCurrentDir to the task file's directory
        # when a task is opened (Delphi Otask.LoadFromFile line: SetCurrentDir).
        # This changes the process-wide CWD, so os.path.abspath() would resolve
        # relative paths against the task directory rather than the caller's CWD.
        # The caller is responsible for passing an already-absolute path, or for
        # resolving it before any DLL call is made.  We still call abspath() here
        # as a safety net for paths that are already absolute (no-op) but we
        # cannot reliably fix truly relative paths at this point.
        abs_filename = os.path.abspath(filename)

        # Create the output directory on the Python side before the DLL call.
        # The DLL's own ForceDirectories (ENFORCE_DIRECTORIES flag) also uses
        # the DLL-changed CWD, so it can fail for relative paths.  Creating the
        # directory here with an absolute path is always reliable.
        if flags & SaveTaskFlags.ENFORCE_DIRECTORIES:
            out_dir = os.path.dirname(abs_filename)
            if out_dir:
                os.makedirs(out_dir, exist_ok=True)

        logger.info(f"Saving task {n} to: {abs_filename} (flags={flags})")

        with _dll_lock:
            if use_unicode:
                result = self.lib.SQ4SaveTask2(n, ctypes.c_wchar_p(abs_filename), c_int(flags))
            else:
                result = self.lib.SQ4SaveTask1(n, ctypes.c_char_p(abs_filename.encode('mbcs')), c_int(flags))

        if result != SQErrorCode.OK:
            raise SQAPITaskError(
                format_error_message(result, f"filename: {abs_filename}"),
                error_code=result,
            )

        logger.info(f"Task {n} saved successfully to: {abs_filename}")

    def finalize(self) -> None:
        """Finalize the SQ API and release all resources."""
        logger.info("Finalizing API")
        
        with _dll_lock:
            result = self.lib.SQ4Finalize()
        
        self._initialized = False
        
        if result != SQErrorCode.OK:
            raise SQAPIError(format_error_message(result, "operation: finalize"), error_code=result)
        
        logger.info("API finalized successfully")

    # High-level convenience methods
    
    def refine(
        self,
        job_handle: int,
        params: Optional[RefinementParams] = None,
        timeout: float = 300.0,
        check_interval: float = 1.0
    ) -> RefinementResult:
        """Refine a task synchronously.
        
        Starts refinement, waits for completion, and returns results.
        
        Args:
            job_handle: Job handle from open_task().
            params: Refinement parameters. If None, the task's stored
                configuration is used unchanged (AutoPreScale, stages, etc.).
            timeout: Maximum wait time in seconds.
            check_interval: Time between status checks.
        
        Returns:
            RefinementResult object with analysis results.
        """
        validate_job_handle(job_handle)
        
        self.start_refinement(job_handle, params)
        status = self._poll_refinement(job_handle, timeout, check_interval)
        
        if status != SQErrorCode.TASK_COMPLETED:
            raise SQAPIRefinementError(
                format_error_message(status, f"job_handle: {job_handle}"),
                error_code=status
            )
        
        return self.report_results(job_handle)

    # Async methods
    
    async def wait_for_refinement(
        self,
        job_handle: int,
        timeout: float = 300.0,
        check_interval: float = 1.0
    ) -> int:
        """Wait for refinement to complete asynchronously.
        
        Args:
            job_handle: Job handle.
            timeout: Maximum wait time in seconds.
            check_interval: Time between status checks.
        
        Returns:
            Final refinement status code.
        
        Raises:
            TimeoutError: If refinement doesn't complete within timeout.
        """
        validate_job_handle(job_handle)
        logger.info(f"Waiting for refinement (timeout={timeout}s)")
        
        start_time = time.time()
        while time.time() - start_time < timeout:
            status = self.check_refinement(job_handle)
            
            if status == SQErrorCode.TASK_COMPLETED:
                return status
            elif status == SQErrorCode.REFINEMENT_FAILED:
                return status
            elif status not in (SQErrorCode.TASK_REFINING, SQErrorCode.TASK_NOT_STARTED):
                return status
            
            await asyncio.sleep(check_interval)
        
        raise TimeoutError(f"Refinement did not complete within {timeout} seconds")

    async def refine_async(
        self,
        job_handle: int,
        params: Optional[RefinementParams] = None,
        timeout: float = 300.0,
        check_interval: float = 1.0
    ) -> RefinementResult:
        """Start refinement and wait for completion asynchronously.
        
        Args:
            job_handle: Job handle.
            params: Refinement parameters. If None, the task's stored
                configuration is used unchanged.
            timeout: Maximum wait time in seconds.
            check_interval: Time between status checks.
        
        Returns:
            RefinementResult object with analysis results.
        """
        self.start_refinement(job_handle, params)
        status = await self.wait_for_refinement(job_handle, timeout, check_interval)
        
        if status != SQErrorCode.TASK_COMPLETED:
            raise SQAPIRefinementError(
                format_error_message(status, f"job_handle: {job_handle}"),
                error_code=status
            )
        
        return self.report_results(job_handle)

    # Context manager support
    
    def __enter__(self) -> 'SQAPI':
        """Context manager entry point."""
        return self
    
    def __exit__(self, exc_type: Optional[type], exc_val: Optional[Exception], exc_tb: Optional[Any]) -> bool:
        """Context manager exit point. Finalizes API automatically."""
        if self._initialized:
            try:
                self.finalize()
            except Exception as e:
                logger.warning(f"Error during API cleanup: {e}")
        return False
    
    def __repr__(self) -> str:
        return f"SQAPI(initialized={self._initialized}, lib_dir_set={self._lib_dir_set})"
