"""
Git integration for Microsoft Fabric workspaces.

Provides functionality for Git synchronization operations including
status checks, updates from Git, and commits to Git.
"""

from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union

from .._shared.exceptions import FabiasError
from .._shared.response import AzResponse
from ._shared.enums import GitProviderType, InitializationStrategy

if TYPE_CHECKING:
    from ._client import FabricClient
    from .connections import Connection


class GitItem:
    """
    Represents a workspace item in Git sync operations.

    This class captures the metadata for items that have changes between
    the workspace and Git repository. Used for both status reporting and
    selective commits.

    Attributes:
        object_id (str): Item's object ID (workspace identifier)
        logical_id (str): Item's logical ID (git repository identifier)
        type (str): Type of item (Report, SemanticModel, etc.)
        name (str): Human-readable name
        workspace_change (str): Change type in workspace (Added, Modified, Deleted, None)
        remote_change (str): Change type in remote (Added, Modified, Deleted, None)
        conflict_type (str): Type of conflict if any (None, Update-Update, etc.)
    """

    def __init__(self, data: Dict[str, Any]):
        """
        Initialize from API response data.

        Args:
            data: Item change data from git status response
        """
        metadata = data.get("itemMetadata", {})
        identifier = metadata.get("itemIdentifier", {})

        # Identifiers
        self.objectid = identifier.get("objectId")
        self.logicalid = identifier.get("logicalId")

        # Metadata
        self.type = metadata.get("itemType")
        self.name = metadata.get("displayName")

        # Change tracking
        class change:
            workspace: str
            remote: str

        self.change = change()
        self.change.workspace = data.get("workspaceChange", "None")
        self.change.remote = data.get("remoteChange", "None")
        self.conflict = data.get("conflictType", "None")

    @property
    def hasConflict(self) -> bool:
        """Check if item has a merge conflict."""
        return self.conflict != "None"

    @property
    def isWorkspaceOnly(self) -> bool:
        """Check if item exists only in workspace (Added in workspace)."""
        return self.change.workspace == "Added" and self.change.remote == "None"

    @property
    def isRemoteOnly(self) -> bool:
        """Check if item exists only in remote (Added in remote)."""
        return self.change.remote == "Added" and self.change.workspace == "None"

    @property
    def isModified(self) -> bool:
        """Check if item is modified in either workspace or remote."""
        return self.change.workspace == "Modified" or self.change.remote == "Modified"

    @property
    def identifier(self) -> Dict[str, str]:
        """
        Convert to dictionary format required by commitToGit API.

        Returns:
            dict: Dictionary with objectId and optionally logicalId
        """
        result = {}
        if self.objectid:
            result["objectId"] = self.objectid
        if self.logicalid:
            result["logicalId"] = self.logicalid
        return result

    def __repr__(self) -> str:
        changes = []
        if self.change.workspace != "None":
            changes.append(f"workspace={self.change.workspace}")
        if self.change.remote != "None":
            changes.append(f"remote={self.change.remote}")

        return (
            f"GitItem({self.name}, "
            f"identifier={self.identifier}, "
            f"type={self.type}, "
            f"{', '.join(changes)})"
        )


class GitStatus:
    """
    Represents the Git status of a Fabric workspace.

    Attributes:
        head (str): Current workspace head commit hash
        remoteCommitHash (str): Latest remote commit hash
        changes (List[GitItem]): List of item changes between workspace and remote
        syncStatus (str): Overall sync status
    """

    def __init__(self, data: Dict[str, Any]):
        """
        Initialize from API response data.

        Args:
            data: Git status response from API
        """
        self.head = data.get("workspaceHead")
        self.remoteCommitHash = data.get("remoteCommitHash")
        self.changes = [GitItem(item) for item in data.get("changes", [])]
        self.syncStatus = data.get("syncStatus")

    @property
    def has_changes(self) -> bool:
        """Check if there are any changes to sync."""
        return len(self.changes) > 0

    @property
    def is_synced(self) -> bool:
        """Check if workspace is fully synced with remote (no changes and commits match)."""
        return self.head == self.remoteCommitHash and not self.has_changes

    @property
    def workspace_changes(self) -> List[GitItem]:
        """Get items with changes in workspace (to be pushed)."""
        return [item for item in self.changes if item.change.workspace != "None"]

    @property
    def remote_changes(self) -> List[GitItem]:
        """Get items with changes in remote (to be pulled)."""
        return [item for item in self.changes if item.change.remote != "None"]

    @property
    def conflicts(self) -> List[GitItem]:
        """Get items with merge conflicts."""
        return [item for item in self.changes if item.hasConflict]

    def __repr__(self) -> str:
        return (
            f"GitStatus(synced={self.isSynced}, "
            f"changes={len(self.changes)}, "
            f"head={self.head[:8] if self.head else None})"
        )


class GitConnection:
    """
    Represents a Git connection for a Fabric workspace.

    Attributes:
        branch_name (str): Git branch name
        repository_name (str): Repository name
        directory_name (str): Directory path in repo
        provider_type (str): Git provider (GitHub, AzureDevOps, etc.)
        owner_name (str): Organization/owner name
        custom_domain_name (str): Custom domain if configured
        head (str): Current commit SHA
        last_sync_time (str): Last sync timestamp (ISO 8601)
        connection_state (str): Connection state (ConnectedAndInitialized, etc.)
        raw_data (dict): Original JSON response from API
    """

    def __init__(self, data: Dict[str, Any]):
        """
        Initialize from API response data.

        Args:
            data: Git connection response from API
        """
        details = data.get("gitProviderDetails", {})
        sync_details = data.get("gitSyncDetails", {})

        # Provider details
        self.branch = details.get("branchName")
        self.repository = details.get("repositoryName")
        self.directory = details.get("directoryName")
        self.provider = details.get("gitProviderType")
        self.owner = details.get("ownerName")
        self.domain = details.get("customDomainName")

        # Sync details
        self.head = sync_details.get("head")
        self.lastSync = sync_details.get("lastSyncTime")

        # Connection state
        self.state = data.get("gitConnectionState")

    @property
    def is_connected(self) -> bool:
        """Check if workspace is connected to Git."""
        return self.repository is not None

    def __repr__(self) -> str:
        if self.is_connected:
            return (
                f"GitConnection(repo={self.repository}, "
                f"branch={self.branch}, "
                f"state={self.state})"
            )
        return "GitConnection(not connected)"


class GitCredentials:
    """
    Represents Git credentials configuration for a workspace.

    Attributes:
        source (str): Credential source ("Automatic", "None", or "ConfiguredConnection")
        connection_id (str): Connection ID if source is "ConfiguredConnection"
        connection (Connection): Connection object if source is "ConfiguredConnection"
    """

    def __init__(self, data: Dict[str, Any], client: Optional["FabricClient"] = None):
        """
        Initialize from API response data.

        Args:
            data: Git credentials response from API
            client: Optional FabricClient for loading Connection object
        """
        self.source = data.get("source")
        self.connection_id = data.get("connectionId")
        self._client = client
        self._connection: Optional["Connection"] = None

    @property
    def is_configured(self) -> bool:
        """Check if credentials are configured (not "None")."""
        return self.source != "None"

    @property
    def uses_connection(self) -> bool:
        """Check if using a configured connection."""
        return self.source == "ConfiguredConnection"

    @property
    def connection(self) -> Optional["Connection"]:
        """
        Get the Connection object if credentials use ConfiguredConnection.

        Returns:
            Connection: The configured connection, or None if not using connection
        """
        if not self.uses_connection or not self.connection_id:
            return None

        if self._connection is None and self._client:
            from .connections import Connection

            self._connection = Connection(self._client, identifier=self.connection_id)

        return self._connection

    def __repr__(self) -> str:
        if self.uses_connection:
            return f"GitCredentials(source={self.source}, connection_id={self.connection_id})"
        return f"GitCredentials(source={self.source})"


class GitCredentialsAccessor:
    """Accessor for Git credentials operations."""

    def __init__(self, client: "FabricClient", workspace_id: str):
        """Initialize credentials accessor."""
        self._client = client
        self._endpoint = f"/workspaces/{workspace_id}/git/myGitCredentials"

    def __call__(self) -> GitCredentials:
        """
        Get the current Git credentials configuration.

        Returns:
            GitCredentials: Credentials object with source and optional connection

        Examples:
            >>> creds = git.credentials()
            >>>
            >>> # Check credential type
            >>> if creds.source == "Automatic":
            ...     print("Using automatic credentials")
            >>> elif creds.uses_connection:
            ...     print(f"Using connection: {creds.connection.name}")
            >>> elif creds.source == "None":
            ...     print("No credentials configured")
        """
        response = self._client.get(self._endpoint)
        data = response.json()
        return GitCredentials(data, client=self._client)

    def update(
        self, connection: Optional["Connection"] = None, source: str = "Automatic"
    ) -> GitCredentials:
        """
        Update the Git credentials configuration.

        Args:
            connection: Optional Connection object to use for Git authentication
            source: Credential source if not using connection ("Automatic" or "None")

        Returns:
            GitCredentials: Updated credentials object

        Examples:
            >>> # Use a configured connection
            >>> conn = fabric.connection("GIT-FLOW-SA")
            >>> git.credentials.update(connection=conn)
            >>>
            >>> # Use automatic credentials
            >>> git.credentials.update(source="Automatic")
            >>>
            >>> # Clear credentials
            >>> git.credentials.update(source="None")
        """
        if connection:
            payload = {"source": "ConfiguredConnection", "connectionId": connection.id}
        else:
            payload = {"source": source}

        self._client.patch(self._endpoint, data=payload)

        # Return updated credentials
        return self()


class Git:
    """
    Manages Git integration for a Microsoft Fabric workspace.

    Provides methods to:
    - Connect/disconnect workspace to/from Git repository
    - Initialize Git connections
    - Check Git sync status
    - Update workspace from Git (pull)
    - Commit workspace changes to Git (push)
    - Configure Git credentials

    Examples:
        >>> workspace = client.workspace()
        >>> git = workspace.git
        >>>
        >>> # Connect to repository
        >>> git.connect("https://github.com/org/repo", "main")
        >>> git.initialize_connection().wait()
        >>>
        >>> # Check status
        >>> status = git.status()
        >>> print(f"Changes: {len(status.changes)}")
        >>>
        >>> # Update from Git
        >>> if status.has_changes:
        ...     operation = git.update_from_git()
        ...     operation.wait()
    """

    def __init__(self, client: "FabricClient", workspace_id: str):
        """
        Initialize Git handler for a workspace.

        Args:
            client: Authenticated FabricClient
            workspace_id: Workspace GUID
        """
        self._client = client
        self._workspace_id = workspace_id
        self._endpoint = f"/workspaces/{workspace_id}/git"

        # Cached status
        self._status: Optional[GitStatus] = None

    @property
    def credentials(self) -> GitCredentialsAccessor:
        """
        Access Git credentials for this workspace.

        Returns:
            GitCredentialsAccessor: Accessor for getting and updating credentials

        Examples:
            Get credentials:

            >>> creds = git.credentials()
            >>> print(f"Source: {creds.source}")

            Update credentials:

            >>> conn = fabric.connection("GIT-FLOW-SA")
            >>> git.credentials.update(connection=conn)
        """
        return GitCredentialsAccessor(self._client, self._workspace_id)

    def connection(self) -> GitConnection:
        """
        Get Git connection information for this workspace.

        Returns:
            GitConnection: Connection details with repository, branch, provider info

        Examples:
            >>> git = workspace.git
            >>> conn = git.connection()
            >>> if conn.is_connected:
            ...     print(f"Repo: {conn.repository}")
            ...     print(f"Branch: {conn.branch}")
        """
        endpoint = f"/workspaces/{self._workspace_id}/git/connection"

        try:
            response = self._client.get(endpoint)
            data = response.json()
            return GitConnection(data)
        except Exception as e:
            if "404" in str(e):
                # Return empty connection (not connected)
                return GitConnection({"gitProviderDetails": {}})
            raise

    def connect(
        self,
        repository_url: str,
        branch: str,
        directory: str = "/",
        connection: Optional["Connection"] = None,
        provider_type: Optional[Union[GitProviderType, str]] = None,
    ) -> "Git":
        """
        Connect workspace to a Git repository and branch.

        Args:
            repository_url: Full Git repository URL (e.g., https://github.com/owner/repo)
            branch: Branch name to connect to
            directory: Directory path within repository (default: root "/")
            connection: Optional Connection object for authenticated access (e.g., for private repos)

        Returns:
            Git: Self for method chaining

        Examples:
            >>> # Public repository
            >>> git.connect(
            ...     repository_url="https://github.com/myorg/myrepo",
            ...     branch="main",
            ...     directory="/fabric-workspace"
            ... )

            >>> # Private repository with connection
            >>> conn = fabric.connection("GIT-FLOW-SA")
            >>> git.connect(
            ...     repository_url="https://github.com/myorg/private-repo",
            ...     branch="main",
            ...     connection=conn
            ... )
        """
        # Determine provider type
        if provider_type:
            # Convert enum to string if needed
            provider = (
                provider_type.value if isinstance(provider_type, GitProviderType) else provider_type
            )
        else:
            # Auto-detect from URL
            provider = (
                "AzureDevOps"
                if "dev.azure.com" in repository_url or "visualstudio.com" in repository_url
                else "GitHub"
            )

        payload = {
            "gitProviderDetails": {
                "gitProviderType": provider,
                "branchName": branch,
                "directoryName": directory,
            }
        }

        # Add provider-specific fields
        if "dev.azure.com" in repository_url or "visualstudio.com" in repository_url:
            # Azure DevOps
            payload["gitProviderDetails"]["organizationName"] = (
                repository_url.split("/")[-4]
                if "dev.azure.com" in repository_url
                else repository_url.split("/")[-3]
            )
            payload["gitProviderDetails"]["projectName"] = (
                repository_url.split("/")[-3]
                if "dev.azure.com" in repository_url
                else repository_url.split("/")[-2]
            )
            payload["gitProviderDetails"]["repositoryName"] = repository_url.split("/")[-1].replace(
                ".git", ""
            )
        else:
            # GitHub
            payload["gitProviderDetails"]["ownerName"] = repository_url.split("/")[-2]
            payload["gitProviderDetails"]["repositoryName"] = repository_url.split("/")[-1].replace(
                ".git", ""
            )

        # Add connection details if provided
        if connection:
            if not connection.id:
                raise ValueError("Connection ID is required")
            payload["myGitCredentials"] = {
                "source": "ConfiguredConnection",
                "connectionId": connection.id,
            }

        try:
            self._client.post(f"{self._endpoint}/connect", data=payload)
        except Exception as e:
            # Handle 409 Conflict - workspace already connected to Git
            from .._shared.exceptions import ApiError

            if (isinstance(e, ApiError) and e.status_code == 409) or (
                "409" in str(e) and "already connected" in str(e).lower()
            ):
                # Already connected - fetch existing connection instead
                return self
            raise

        return self

    def disconnect(self) -> "Git":
        """
        Disconnect workspace from Git repository.

        Removes the Git connection from this workspace. Does not affect
        the remote repository or any files.

        Returns:
            Git: Self for method chaining

        Examples:
            >>> workspace.git.disconnect()
        """
        self._client.post(f"{self._endpoint}/disconnect")
        return self

    def initialize(
        self, initialization_strategy: Optional[Union[InitializationStrategy, str]] = None
    ) -> AzResponse:
        """
        Initialize a Git connection for a workspace that's connected to Git.

        This initializes the connection after calling connect(), preparing
        the workspace for sync operations.

        Args:
            initialization_strategy: Strategy when content exists on both sides.
                Can be an InitializationStrategy enum or string:
                - None (default): No strategy defined
                - InitializationStrategy.PREFER_REMOTE or "PreferRemote": Prefer remote Git content
                - InitializationStrategy.PREFER_WORKSPACE or "PreferWorkspace": Prefer workspace content

        Returns:
            Operation: Operation object (either immediate or long-running)
                For immediate operations (200 response), contains:
                - required_action: Next action needed (RequiredAction enum or string)
                - remote_commit_hash: Remote full SHA commit hash
                - workspace_head: Full SHA hash that workspace is synced to

        Examples:
            >>> # Using enum (recommended - provides IntelliSense)
            >>> from fabias.fabric.enums import InitializationStrategy
            >>> git.connect(repo_url, "main")
            >>> op = git.initialize(InitializationStrategy.PREFER_REMOTE)
            >>> if op.isImmediate:
            ...     print(f"Required action: {op.required_action}")

            >>> # Using string (also supported)
            >>> op = git.initialize("PreferRemote")
        """
        payload = {}
        if initialization_strategy:
            # Convert enum to string value if needed
            strategy_value = (
                initialization_strategy.value
                if isinstance(initialization_strategy, InitializationStrategy)
                else initialization_strategy
            )
            payload["initializationStrategy"] = strategy_value

        response = self._client.post(
            f"{self._endpoint}/initializeConnection", data=payload if payload else None
        )

        # Response is already an AzResponse from BaseClient._handle_response()
        return response

    def status(self, force_refresh: bool = False) -> GitStatus:
        """
        Get the current Git sync status.

        Args:
            force_refresh: If True, always fetch from API. If False,
                may return cached status.

        Returns:
            GitStatus: Current sync status with changes list

        Raises:
            FabiasError: If Git credentials not configured or API fails
        """
        if self._status is None or force_refresh:
            response = self._client.get(f"{self._endpoint}/status")
            data = response.json()

            # Check for configuration errors
            if data.get("errorCode") == "GitCredentialsNotConfigured":
                raise FabiasError("Git credentials not configured. Use set_connection() first.")

            self._status = GitStatus(data)

        return self._status

    def pull(
        self,
        conflict_resolution: str = "PreferRemote",
        allow_override: bool = True,
        head: Optional[str] = None,
        remote_commit: Optional[str] = None,
    ) -> AzResponse:
        """
        Update the workspace from Git (pull).

        Initiates a long-running operation to sync the workspace with
        the remote Git repository.

        Args:
            conflict_resolution: How to handle conflicts:
                - "PreferRemote": Use remote version (default)
                - "PreferWorkspace": Keep workspace version
            allow_override: Whether to allow overwriting workspace items
            head: Workspace commit hash (uses current if not provided)
            remote_commit: Remote commit to sync to (uses latest if not provided)

        Returns:
            Operation: Long-running operation for tracking progress

        Examples:
            >>> operation = git.update_from_git()
            >>> operation.wait(callback=lambda op: print(f"Progress: {op.percent_complete}%"))
            >>> print("Sync complete!")
        """
        # Get current status if not provided
        if not head or not remote_commit:
            status = self.status(force_refresh=True)
            head = head or status.head
            remote_commit = remote_commit or status.remoteCommitHash

        payload = {
            "workspaceHead": head,
            "remoteCommitHash": remote_commit,
            "conflictResolution": {
                "conflictResolutionType": "Workspace",
                "conflictResolutionPolicy": conflict_resolution,
            },
            "options": {"allowOverrideItems": allow_override},
        }

        response = self._client.post(f"{self._endpoint}/updateFromGit", data=payload)

        # Response is already an AzResponse from BaseClient._handle_response()
        return response

    def commitAndPush(
        self, comment: str, items: Optional[List[GitItem]] = None, head: Optional[str] = None
    ) -> AzResponse:
        """
        Commit workspace changes to Git (push).

        Initiates a long-running operation to commit changes from the
        workspace to the remote Git repository.

        Args:
            comment: Commit message
            items: List of GitItem objects to commit. Typically obtained from git.status().
                   If None, commits all pending changes.
            head: Workspace commit hash (uses current if not provided)

        Returns:
            Operation: Long-running operation for tracking progress

        Examples:
            >>> # Commit all changes
            >>> git.commitAndPush("Update reports")
            >>>
            >>> # Commit specific items
            >>> status = git.status()
            >>> workspace_items = status.workspaceChanges
            >>> git.commitAndPush("Add new datasets", items=workspace_items)
        """
        if not head:
            status = self.status(force_refresh=True)
            head = status.head

        payload: Dict[str, Any] = {
            "mode": "All" if items is None else "Selective",
            "workspaceHead": head,
            "comment": comment,
        }

        if items:
            # Convert GitItem objects to dict format required by API
            payload["items"] = [item.identifier for item in items]

        response = self._client.post(f"{self._endpoint}/commitToGit", data=payload)

        # Response is already an AzResponse from BaseClient._handle_response()
        return response

    def __repr__(self) -> str:
        return f"Git(workspace_id={self._workspace_id})"
