"""
Long-running job tracking for asynchronous operations.

Provides unified AzResponse class for both Azure Data Factory and Microsoft Fabric
long-running operations, handling different response formats and result endpoints.
"""

from dataclasses import dataclass
from datetime import datetime, timezone
from time import sleep
from typing import TYPE_CHECKING, Callable, Optional

import requests

from .utils import coalesce

if TYPE_CHECKING:
    from .client import BaseClient

# Terminal status values across both APIs
TERMINAL_STATES = {"completed", "failed", "succeeded", "cancelled"}


@dataclass
class Job:
    """
    Represents a long-running operation/job.

    This is a simplified data structure for job tracking attributes.
    Used internally by AzResponse to maintain job state.
    """

    id: Optional[str] = "Immediate"
    location: Optional[str] = None
    status: Optional[str] = "initialized"
    message: Optional[str] = None
    percent: int = 0
    retry: int = 30
    start: Optional[str] = None
    elapsed: Optional[float] = None
    end: Optional[str] = None
    error: Optional[dict] = None

    def parse(self, response: "AzResponse", status: Optional[str] = None) -> "Job":
        try:
            data = response.json() or {}
        except (ValueError, TypeError, AttributeError):
            data = {}

        # Parse job ID from body or headers (ADF vs Fabric patterns)
        self.id = coalesce(data, "id", "runId", default=self.id)
        # Check headers for operation IDs (convert headers to dict for coalesce)
        header_dict = dict(response.headers) if hasattr(response.headers, "items") else {}
        self.id = coalesce(
            header_dict, "x-ms-operation-id", "x-ms-job-id", "Operation-Id", default=self.id
        )

        # Parse status from body or use provided status
        # Priority: explicit status param (if not "unknown") > body data > current status
        body_status = coalesce(data, "status", "state", default=None)
        if status and status != "unknown":
            self.status = status
        elif body_status:
            self.status = body_status
        # else: keep current self.status

        # Parse retry interval from headers
        retry = coalesce(data.get("retry"), response.headers.get("Retry-After"), default=self.retry)
        try:
            self.retry = int(retry)
        except ValueError:
            pass
        if self.retry > 300:
            self.retry = self.retry // 1000

        # Parse message/error (ADF uses 'failureReason'/'message', Fabric uses 'error' object)
        self.message = coalesce(data, "failureReason", "message", self.message)
        if isinstance(data.get("error"), dict):
            self.message = data["error"].get("message", self.message)
            self.error = data["error"]

        # Process Location for next call
        self.location = coalesce(
            response.headers.get("Location"),
            self.location,
            f"https://api.fabric.microsoft.com/v1/operations/{self.id}",
        )

        # Calculate time and durations
        if not self.start:
            self.start = coalesce(
                data, "startTimeUtc", "runStart", default=datetime.now(timezone.utc).isoformat()
            )

        # Calculate elapsed time
        if self.start:
            try:
                start_time = datetime.fromisoformat(self.start.replace("Z", "+00:00"))
                self.elapsed = (datetime.now(timezone.utc) - start_time).total_seconds()
            except (ValueError, AttributeError):
                pass

        if self.status in TERMINAL_STATES:
            self.end = coalesce(data, "endTimeUtc", "runEnd", default=self.end)
            self.percent = 100

        self.percent = coalesce(data.get("percentComplete"), self.percent)

        return self


class AzResponse(requests.Response):
    """
    Response object for long-running asynchronous operations.

    Extends requests.Response with job tracking capabilities. Handles both
    Azure Data Factory (response body) and Microsoft Fabric (response headers)
    operation patterns.

    Attributes:
        id (str): Unique operation/job identifier
        status (str): Current job status (initialized, running, succeeded, failed, etc.)
        message (str): Status message or error description
        error (dict): Error information if failed
        complete (bool): Whether job has reached terminal state
        successful (bool): Whether job completed successfully

    Examples:
        >>> # Returns AzResponse for 200/202 automatically
        >>> response = client.post("/pipelines/run")
        >>> response.wait(callback=lambda r: print(f"Status: {r.status}"))
        >>> result = response.result()  # Fetch final result
    """

    def __init__(self, response: requests.Response, client: "BaseClient"):
        """
        Create AzResponse from existing requests.Response.

        Copies all internal state from the original response and parses job details.

        Args:
            response: Original requests.Response object
            client: BaseClient for making status check requests
        """
        super().__init__()

        # Copy all response attributes
        self.status_code = response.status_code
        self.headers = response.headers
        self._content = response._content
        self.url = response.url
        self.encoding = response.encoding
        self.reason = response.reason
        self.cookies = response.cookies
        self.elapsed = response.elapsed
        self.request = response.request
        self.history = response.history

        # Add job tracking attributes
        self._client = client
        self._result_location: Optional[str] = None
        self.longRunning = False
        self.data: Optional[dict] = {}

        # Core job attributes (prefixed with job_ to avoid conflicts)
        self.job: Job = Job()

        # Parse job details from response
        self._parse_response()

    def _parse_response(self) -> None:
        """
        Parse job details from HTTP response.

        Handles both:
        - 200 OK: Immediate completion OR ADF pipeline run (long-running)
        - 202 Accepted: Long-running operation, details in headers
        """
        # Try to parse body first (needed for ADF detection)
        try:
            self.data = self.json()
        except (ValueError, TypeError, AttributeError):
            self.data = {}

        # Special case: ADF pipeline runs return 200 but are long-running
        # Detect by: status=200 + /createRun in URL + runId in body
        request_url = self.request.url if self.request else None
        if (
            self.status_code in [200, 202]
            and request_url
            and "/createRun" in request_url
            and self.data.get("runId")
        ):
            self.longRunning = True
            self.job.parse(self, status="initialized")

            # Build status check URL
            base_uri = self._client._base_uri
            api_version = "2018-06-01"
            if "api-version=" in request_url:
                api_version = request_url.split("api-version=")[1].split("&")[0]
            self.job.location = f"{base_uri}/pipelineruns/{self.job.id}?api-version={api_version}"
            return

        # Check for immediate completion (200 OK, not ADF createRun)
        if self.status_code in [200, 201, 204]:
            self.job.parse(self, status="succeeded")
            return

        if self.status_code == 202:
            # Long-running operation (202 Accepted)
            self.longRunning = True
            self.job.parse(self, status="initialized")

    def checkStatus(self) -> "AzResponse":
        """
        Check current job status from the API.

        Handles both Azure Data Factory (body-based) and Microsoft Fabric
        (status in body, result location in headers) response formats.

        Returns:
            AzResponse: Self for method chaining
        """
        if not self.job.location:
            raise ValueError("Job location not available for status check")

        response = self._client.get(self.job.location)
        self.job.parse(response, status="unknown")

        # Capture result location from headers (Fabric LRO pattern)
        # When operation succeeds, Location header points to result endpoint
        location_header = response.headers.get("Location")
        if location_header and self.successful:
            self._result_location = location_header

        return self

    @property
    def complete(self) -> bool:
        """
        Check if the job has reached a terminal state.

        Returns:
            bool: True if job is completed, failed, or succeeded
        """
        return (self.job.status or "").lower() in TERMINAL_STATES

    @property
    def successful(self) -> bool:
        """
        Check if the job completed successfully.

        Returns:
            bool: True if job succeeded
        """
        return (self.job.status or "").lower() in {"completed", "succeeded"}

    def wait(
        self,
        callback: Optional[Callable[["AzResponse"], None]] = None,
        timeout: int = 7200,
        raiseFailure: bool = True,
    ) -> "AzResponse":
        """
        Wait for job completion with polling.

        Polls the job status at regular intervals until it reaches a
        terminal state or the timeout is reached.

        Args:
            callback: Function called after each status check with AzResponse instance
            timeout: Maximum seconds to wait (default 2 hours)
            raiseFailure: Whether to raise exception if job fails

        Returns:
            AzResponse: Self with final status

        Raises:
            OperationError: If timeout reached or job fails (when raiseFailure=True)
        """
        if not self.longRunning:
            raise ValueError("Cannot wait on a non-long-running response")

        from .exceptions import OperationError

        max_iterations = max(1, timeout // self.job.retry)
        iteration = 0

        while not self.complete:
            if iteration >= max_iterations:
                raise OperationError(
                    f"Operation timed out after {timeout} seconds",
                    operation_id=self.job.id,
                    operation_status=self.job.status,
                )

            sleep(self.job.retry)
            self.checkStatus()

            if callback and callable(callback):
                callback(self)

            iteration += 1

        if raiseFailure and not self.successful:
            error_msg = self.job.message or (
                self.job.error.get("message") if self.job.error else "Unknown error"
            )
            raise OperationError(
                f"Operation failed: {error_msg}",
                operation_id=self.job.id,
                operation_status=self.job.status,
            )

        return self

    def result(self) -> Optional[dict]:
        """
        Fetch result data from the result endpoint (Fabric pattern).

        Some Fabric operations (like notebook.get_definition()) require fetching
        the result from a separate endpoint immediately after completion.
        The result location is captured from the Location header during status checks.

        Returns:
            dict: Result data from the API, or None if no result endpoint

        Raises:
            FabiasError: If result fetch fails or result has expired

        Examples:
            >>> job = notebook.getDefinition()
            >>> job.wait()
            >>> definition = job.result()  # Must call immediately after wait()
        """
        from .exceptions import FabiasError

        if not self.successful:
            raise FabiasError(
                f"Cannot fetch result - job status is '{self.job.status}', not succeeded"
            )

        # Determine result URL
        # Priority: 1) Location header from last check, 2) Append /result to status URL
        result_url = self._result_location
        if not result_url and self.job.location:
            # Fabric pattern: operation status at /operations/{id}, result at /operations/{id}/result
            result_url = (
                f"{self.job.location}/result"
                if not self.job.location.endswith("/result")
                else self.job.location
            )

        if not result_url:
            raise FabiasError("No result endpoint available for this operation")

        try:
            response = self._client.get(result_url)
            return response.json()
        except Exception as e:
            raise FabiasError(f"Failed to fetch operation result: {str(e)}") from e

    def __repr__(self) -> str:
        """Return string representation."""
        return (
            f"AzResponse(id={self.id}, status={self.status}, "
            f"message={self.message!r}, percent={self.percent}%)"
        )
