"""
CRM API Module
==============

This module provides functionality to interact with the Dynamics CRM SOAP API
for querying users and executing state changes (termination).
"""

import requests
import logging
from typing import Dict, Optional, List
from urllib.parse import urljoin

logger = logging.getLogger(__name__)


class CRMAPIError(Exception):
    """Exception raised for CRM API errors"""
    pass


class CRMClient:
    """
    Client for interacting with Dynamics CRM SOAP API.

    This client handles authentication, user queries, and user state changes.
    """

    def __init__(self, base_url: str, cookies: Dict[str, str], timeout: int = 30):
        """
        Initialize the CRM client.

        Args:
            base_url: Base URL of the CRM instance (e.g., https://internalcrm.macausjm-glp.com)
            cookies: Dictionary of session cookies (must include ReqClientId and orgId)
            timeout: Request timeout in seconds
        """
        self.base_url = base_url.rstrip('/')
        self.cookies = cookies
        self.timeout = timeout
        self.api_endpoint = urljoin(self.base_url, '/MDP/AppWebServices/InlineEditWebService.asmx')

        # Validate required cookies
        if 'ReqClientId' not in cookies or 'orgId' not in cookies:
            raise ValueError("Cookies must include 'ReqClientId' and 'orgId'")

        # Common headers
        self.headers = {
            'Accept': 'application/xml, text/xml, */*; q=0.01',
            'Accept-Encoding': 'gzip, deflate, br',
            'Accept-Language': 'en-US,en;q=0.9',
            'ClientAppName': 'WebClient',
            'ClientAppVersion': '9.0',
            'Content-Type': 'text/xml; charset=UTF-8',
            'Origin': self.base_url,
            'Referer': f'{self.base_url}/MDP/main.aspx',
            'SOAPAction': 'http://schemas.microsoft.com/xrm/2011/Contracts/Services/IOrganizationService/Execute',
            'X-Requested-With': 'XMLHttpRequest',
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36'
        }

    def _build_soap_envelope(self, body_xml: str) -> str:
        """
        Build a complete SOAP envelope.

        Args:
            body_xml: The SOAP body content

        Returns:
            str: Complete SOAP envelope
        """
        return f'''<?xml version="1.0" encoding="utf-8"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
  <s:Header>
    <SdkClientVersion xmlns="http://schemas.microsoft.com/xrm/2011/Contracts">9.0</SdkClientVersion>
  </s:Header>
  <s:Body>
    {body_xml}
  </s:Body>
</s:Envelope>'''

    def _execute_request(self, soap_body: str) -> str:
        """
        Execute a SOAP request against the CRM API.

        Args:
            soap_body: The SOAP body content

        Returns:
            str: The response XML

        Raises:
            CRMAPIError: If the request fails
        """
        envelope = self._build_soap_envelope(soap_body)

        try:
            response = requests.post(
                self.api_endpoint,
                data=envelope.encode('utf-8'),
                headers=self.headers,
                cookies=self.cookies,
                timeout=self.timeout
            )

            response.raise_for_status()
            return response.text

        except requests.exceptions.Timeout:
            raise CRMAPIError(f"Request timeout after {self.timeout} seconds")
        except requests.exceptions.HTTPError as e:
            raise CRMAPIError(f"HTTP error: {e}")
        except requests.exceptions.RequestException as e:
            raise CRMAPIError(f"Request failed: {e}")

    def find_user_by_display_name(self, display_name: str) -> Optional[Dict[str, str]]:
        """
        Find a CRM user by display name.

        Args:
            display_name: The user's display name (fullname)

        Returns:
            dict: User information including systemuserid and domainname, or None if not found

        Raises:
            CRMAPIError: If the query fails
        """
        # Use RetrieveMultiple with QueryExpression to find user by fullname
        soap_body = f'''<Execute xmlns="http://schemas.microsoft.com/xrm/2011/Contracts/Services" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
  <request i:type="a:RetrieveMultipleRequest" xmlns:a="http://schemas.microsoft.com/xrm/2011/Contracts">
    <a:Parameters xmlns:b="http://schemas.datacontract.org/2004/07/System.Collections.Generic">
      <a:KeyValuePairOfstringanyType>
        <b:key>Query</b:key>
        <b:value i:type="a:QueryExpression">
          <a:ColumnName>systemuserid</a:ColumnName>
          <a:Distinct>false</a:Distinct>
          <a:EntityName>systemuser</a:EntityName>
          <a: PageInfo>
            <a:Count>1</a:Count>
            <a:PageNumber>1</a:PageNumber>
            <a:PagingCookie i:nil="true"/>
          </a:PageInfo>
          <a:Criteria>
            <a:Conditions>
              <a:ConditionExpression>
                <a:AttributeName>fullname</a:AttributeName>
                <a:Operator>Equal</a:Operator>
                <a:Values xmlns:c="http://schemas.microsoft.com/2003/10/Serialization/Arrays">
                  <c:anyType i:type="d:string" xmlns:d="http://www.w3.org/2001/XMLSchema">{display_name}</c:anyType>
                </a:Values>
              </a:ConditionExpression>
            </a:Conditions>
            <a:FilterOperator>And</a:FilterOperator>
          </a:Criteria>
          <a:Orders/>
          <a:ColumnSet>
            <a:AllColumns>false</a:AllColumns>
            <a:Columns xmlns:c="http://schemas.microsoft.com/2003/10/Serialization/Arrays">
              <c:string>systemuserid</c:string>
              <c:string>domainname</c:string>
              <c:string>fullname</c:string>
              <c:string>ito_staffno</c:string>
            </a:Columns>
          </a:ColumnSet>
        </b:value>
      </a:KeyValuePairOfstringanyType>
    </a:Parameters>
    <a:RequestId i:nil="true"/>
    <a:RequestName>RetrieveMultiple</a:RequestName>
  </request>
</Execute>'''

        try:
            response_xml = self._execute_request(soap_body)

            # Parse response to check for errors
            if '<faultcode>' in response_xml or '<faultstring>' in response_xml:
                error_match = re.search(r'<faultstring>(.*?)</faultstring>', response_xml, re.DOTALL)
                error_msg = error_match.group(1).strip() if error_match else "Unknown error"
                raise CRMAPIError(f"CRM query error: {error_msg}")

            # Extract systemuserid from response
            # Expected format: <a:Id>GUID</a:Id>
            import re
            guid_match = re.search(r'<a:Id>([A-F0-9-]{36})</a:Id>', response_xml, re.IGNORECASE)

            if not guid_match:
                # User not found
                return None

            systemuserid = guid_match.group(1)

            # Extract domainname if available
            domainname_match = re.search(r'<a:domainname>([^<]+)</a:domainname>', response_xml)
            domainname = domainname_match.group(1) if domainname_match else ""

            # Extract ito_staffno if available
            staffno_match = re.search(r'<a:ito_staffno>([^<]+)</a:ito_staffno>', response_xml)
            staffno = staffno_match.group(1) if staffno_match else ""

            return {
                'systemuserid': systemuserid,
                'domainname': domainname,
                'fullname': display_name,
                'ito_staffno': staffno
            }

        except Exception as e:
            if isinstance(e, CRMAPIError):
                raise
            raise CRMAPIError(f"Error finding user by display name '{display_name}': {e}")

    def get_display_name_variants(self, display_name: str) -> List[str]:
        """
        Generate DisplayName variants for CRM search.

        This method generates a list of DisplayName variants to try when searching
        for a user in CRM. This handles cases where AD terminated users have "-d"
        suffix but CRM may retain the original name.

        Args:
            display_name: The display name from AD (will be stripped of leading/trailing whitespace)

        Returns:
            list: Ordered list of display name variants to try

        Examples:
            >>> get_display_name_variants("John Smith-d")
            ["John Smith-d", "John Smith"]
            >>> get_display_name_variants("John Smith")
            ["John Smith", "John Smith-d"]
        """
        # Strip leading/trailing whitespace first
        display_name = display_name.strip()

        variants = []

        # Always try the original name first
        variants.append(display_name)

        # If name ends with "-d", add version without "-d"
        if display_name.endswith("-d"):
            base_name = display_name[:-2].strip()  # Remove "-d" and strip again
            if base_name and base_name != display_name:
                variants.append(base_name)

        # If name doesn't end with "-d", add version with "-d"
        else:
            terminated_name = display_name + "-d"
            variants.append(terminated_name)

        return variants

    def find_user_by_display_name_and_employee_id_with_fallback(
        self,
        display_name: str,
        employee_id: str
    ) -> Dict[str, str]:
        """
        Find a CRM user by display name with automatic fallback to variants.

        This method tries multiple DisplayName variants to handle cases where
        AD and CRM display names are out of sync (e.g., AD has "-d" suffix
        but CRM retains original name).

        Args:
            display_name: The user's display name from AD
            employee_id: The employee ID to verify

        Returns:
            dict: User information including systemuserid and domainname

        Raises:
            CRMAPIError: If user not found with any variant or EmployeeId doesn't match
        """
        # Generate all possible display name variants
        variants = self.get_display_name_variants(display_name)

        logger.info(f"Searching CRM for user: {display_name} (EmployeeId: {employee_id})")
        logger.info(f"  Display name variants to try: {variants}")

        # Try each variant
        for idx, variant in enumerate(variants, 1):
            logger.info(f"  Attempt {idx}/{len(variants)}: Searching for '{variant}'")

            try:
                # Try to find user with this variant
                user_info = self.find_user_by_display_name(variant)

                if user_info:
                    # Verify EmployeeId matches
                    crm_staffno = user_info.get('ito_staffno', '')

                    if not crm_staffno:
                        logger.warning(f"    ✗ User found but ito_staffno is empty")
                        continue

                    if crm_staffno != employee_id:
                        logger.warning(
                            f"    ✗ EmployeeId mismatch: "
                            f"expected '{employee_id}', CRM has '{crm_staffno}'"
                        )
                        continue

                    # Found and verified!
                    logger.info(
                        f"    ✓ Found and verified: {variant} "
                        f"(CRM ID: {user_info['systemuserid']}, "
                        f"EmployeeId: {crm_staffno})"
                    )

                    # If we used a variant different from original, log it
                    if variant != display_name:
                        logger.info(
                            f"    ℹ Matched using variant '{variant}' "
                            f"instead of original '{display_name}'"
                        )

                    return user_info

            except CRMAPIError as e:
                # This variant didn't work, log and continue
                logger.warning(f"    ✗ Variant '{variant}' not found: {e}")
                continue

        # All variants failed
        raise CRMAPIError(
            f"User not found in CRM with any display name variant.\n"
            f"  Original display name: {display_name}\n"
            f"  Variants tried: {variants}\n"
            f"  EmployeeId: {employee_id}"
        )

    def find_user_by_display_name_and_employee_id(
        self,
        display_name: str,
        employee_id: str
    ) -> Optional[Dict[str, str]]:
        """
        Find a CRM user by display name and verify the EmployeeId matches.

        This method first finds the user by display name, then verifies that
        the ito_staffno field matches the provided employee_id.

        Args:
            display_name: The user's display name (fullname)
            employee_id: The employee ID to verify

        Returns:
            dict: User information including systemuserid and domainname

        Raises:
            CRMAPIError: If user not found or EmployeeId doesn't match
        """
        logger.info(f"Searching CRM for user: {display_name} with EmployeeId: {employee_id}")

        user_info = self.find_user_by_display_name(display_name)

        if not user_info:
            raise CRMAPIError(
                f"User not found in CRM with display name '{display_name}'"
            )

        # Verify EmployeeId matches
        crm_staffno = user_info.get('ito_staffno', '')

        if not crm_staffno:
            raise CRMAPIError(
                f"User '{display_name}' found in CRM but ito_staffno field is empty"
            )

        if crm_staffno != employee_id:
            raise CRMAPIError(
                f"EmployeeId mismatch for '{display_name}': "
                f"Expected '{employee_id}', CRM has '{crm_staffno}'"
            )

        logger.info(
            f"User verified: {display_name} (CRM ID: {user_info['systemuserid']}, "
            f"EmployeeId: {crm_staffno})"
        )

        return user_info

    def disable_user(self, systemuserid: str) -> bool:
        """
        Disable a user in CRM using SetStateRequest.

        Args:
            systemuserid: The CRM systemuser ID (GUID)

        Returns:
            bool: True if successful

        Raises:
            CRMAPIError: If the operation fails
        """
        soap_body = f'''<Execute xmlns="http://schemas.microsoft.com/xrm/2011/Contracts/Services" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
  <request i:type="b:SetStateRequest" xmlns:a="http://schemas.microsoft.com/xrm/2011/Contracts" xmlns:b="http://schemas.microsoft.com/crm/2011/Contracts">
    <a:Parameters xmlns:b="http://schemas.datacontract.org/2004/07/System.Collections.Generic">
      <a:KeyValuePairOfstringanyType>
        <b:key>EntityMoniker</b:key>
        <b:value i:type="a:EntityReference">
          <a:Id>{systemuserid}</a:Id>
          <a:LogicalName>systemuser</a:LogicalName>
        </b:value>
      </a:KeyValuePairOfstringanyType>
      <a:KeyValuePairOfstringanyType>
        <b:key>State</b:key>
        <b:value i:type="a:OptionSetValue">
          <a:Value>1</a:Value>
        </b:value>
      </a:KeyValuePairOfstringanyType>
      <a:KeyValuePairOfstringanyType>
        <b:key>Status</b:key>
        <b:value i:type="a:OptionSetValue">
          <a:Value>-1</a:Value>
        </b:value>
      </a:KeyValuePairOfstringanyType>
      <a:KeyValuePairOfstringanyType>
        <b:key>MaintainLegacyAppServerBehavior</b:key>
        <b:value i:type="c:boolean" xmlns:c="http://www.w3.org/2001/XMLSchema">true</b:value>
      </a:KeyValuePairOfstringanyType>
    </a:Parameters>
    <a:RequestId i:nil="true"/>
    <a:RequestName>SetState</a:RequestName>
  </request>
</Execute>'''

        try:
            logger.info(f"Disabling CRM user: {systemuserid}")

            response_xml = self._execute_request(soap_body)

            # Check for errors in response
            if '<faultcode>' in response_xml or '<faultstring>' in response_xml:
                error_match = re.search(r'<faultstring>(.*?)</faultstring>', response_xml, re.DOTALL)
                error_msg = error_match.group(1).strip() if error_match else "Unknown error"
                raise CRMAPIError(f"Failed to disable user: {error_msg}")

            # Success if no errors
            logger.info(f"Successfully disabled CRM user: {systemuserid}")
            return True

        except Exception as e:
            if isinstance(e, CRMAPIError):
                raise
            raise CRMAPIError(f"Error disabling user {systemuserid}: {e}")

    def disable_user_with_retry(
        self,
        systemuserid: str,
        max_retries: int = 3
    ) -> tuple[bool, str]:
        """
        Disable a user with retry logic.

        Args:
            systemuserid: The CRM systemuser ID (GUID)
            max_retries: Maximum number of retry attempts

        Returns:
            tuple: (success: bool, message: str)
        """
        import time

        last_error = None

        for attempt in range(1, max_retries + 1):
            try:
                self.disable_user(systemuserid)
                return True, f"Successfully disabled user (attempt {attempt}/{max_retries})"

            except CRMAPIError as e:
                last_error = str(e)
                logger.warning(
                    f"Attempt {attempt}/{max_retries} failed for user {systemuserid}: {e}"
                )

                if attempt < max_retries:
                    # Wait before retry with exponential backoff
                    wait_time = 2 ** attempt
                    logger.info(f"Waiting {wait_time}s before retry...")
                    time.sleep(wait_time)

        return False, f"Failed after {max_retries} attempts. Last error: {last_error}"


if __name__ == "__main__":
    # Test the CRM API module
    import sys
    import json

    logging.basicConfig(level=logging.INFO)

    if len(sys.argv) < 4:
        print("Usage: python crm_api.py <base_url> <ReqClientId> <orgId> <display_name> <employee_id>")
        print("\nExample:")
        print('  python crm_api.py https://internalcrm.macausjm-glp.com "guid1" "guid2" "John Doe" "162727"')
        sys.exit(1)

    base_url = sys.argv[1]
    req_client_id = sys.argv[2]
    org_id = sys.argv[3]
    display_name = sys.argv[4] if len(sys.argv) > 4 else None
    employee_id = sys.argv[5] if len(sys.argv) > 5 else None

    cookies = {
        'ReqClientId': req_client_id,
        'orgId': org_id
    }

    try:
        client = CRMClient(base_url, cookies)

        if display_name and employee_id:
            print(f"\nSearching for user: {display_name} (EmployeeId: {employee_id})")
            user_info = client.find_user_by_display_name_and_employee_id(display_name, employee_id)
            print(f"Found:")
            print(f"  CRM User ID: {user_info['systemuserid']}")
            print(f"  Domain Name: {user_info['domainname']}")
            print(f"  Full Name: {user_info['fullname']}")
            print(f"  Employee ID: {user_info['ito_staffno']}")

            # Ask if we should disable the user
            response = input("\nDisable this user? (yes/no): ")
            if response.lower() == 'yes':
                success, message = client.disable_user_with_retry(user_info['systemuserid'])
                print(f"\nResult: {message}")

    except (CRMAPIError, ValueError) as e:
        print(f"\nError: {e}")
        sys.exit(1)
