"""
Dynamics 365 Bulk Operations Module
Handles CSV import/export and FetchXML paging
FIXED: URL construction, GUID handling, Import API flow
"""

import asyncio
import base64
import xml.etree.ElementTree as ET
from typing import Dict, List, Optional, Callable, Any
from dataclasses import dataclass
import pandas as pd
import json
import logging
import re
from datetime import datetime

logger = logging.getLogger(__name__)


@dataclass
class ImportJobResult:
    """Result of a bulk import operation"""

    job_id: str
    status: str  # "completed", "failed", "partial"
    records_processed: int = 0
    records_succeeded: int = 0
    records_failed: int = 0
    error_details: Optional[List[Dict[str, Any]]] = None
    completion_time: Optional[datetime] = None


class DynamicsBulkAPI:
    """
    Handles bulk operations for Dynamics 365
    Mirrors the SalesforceBulkAPI interface
    """

    def __init__(self, integration):
        """
        Args:
            integration: DynamicsIntegration instance
        """
        self.integration = integration
        self.page_size = 5000  # Dynamics recommended page size

    async def import_csv(
        self,
        entity: str,
        df: pd.DataFrame,
        col_map: Dict[str, str],
        progress_cb: Optional[Callable[[float, str], None]] = None,
    ) -> ImportJobResult:
        """
        Import CSV data using Dynamics Data Import - FIXED SEQUENCE

        Args:
            entity: Logical name of the entity
            df: DataFrame to import
            col_map: Mapping of CSV columns to Dynamics fields
            progress_cb: Progress callback function

        Returns:
            ImportJobResult with job details
        """
        try:
            if progress_cb:
                progress_cb(0.0, "Preparing import...")

            # Step 1: Get entity metadata for proper EntitySetName
            entity_set = self.integration._get_entity_set_name(entity)

            # Step 2: Create Entity record first (required for import)
            entity_data = {
                "name": entity,
                "source": 1,  # Import
                "description": f"Import entity for {entity}",
            }

            entity_url = (
                f"{self.integration.credentials.resource}/api/data/v9.2/entities"
            )

            async with await self.integration._make_request(
                "POST", entity_url, json=entity_data
            ) as resp:
                if resp.status != 201:
                    # Entity might already exist, which is OK
                    logger.info(f"Entity record might already exist: {resp.status}")
                else:
                    entity_id = (
                        resp.headers.get("OData-EntityId", "").split("(")[1].rstrip(")")
                    )

            # Step 3: Create Import
            import_data = {
                "name": f"FoundryMatch Import {datetime.utcnow().isoformat()}",
                "modecode": 0,  # Create mode
                "entitiesperfile": 1,
            }

            import_url = (
                f"{self.integration.credentials.resource}/api/data/v9.2/imports"
            )

            async with await self.integration._make_request(
                "POST", import_url, json=import_data
            ) as resp:
                if resp.status != 201:
                    error_text = await resp.text()
                    raise Exception(f"Failed to create import job: {error_text}")

                import_id = (
                    resp.headers.get("OData-EntityId", "").split("(")[1].rstrip(")")
                )

            if progress_cb:
                progress_cb(0.2, "Import job created...")

            # Step 4: Create ImportFile with proper CSV content
            # Remap columns according to col_map
            mapped_df = pd.DataFrame()
            for csv_col, dynamics_field in col_map.items():
                if csv_col in df.columns:
                    mapped_df[dynamics_field] = df[csv_col]

            csv_content = mapped_df.to_csv(index=False)
            csv_base64 = base64.b64encode(csv_content.encode("utf-8")).decode("utf-8")

            import_file_data = {
                "name": f"{entity}_import.csv",
                "content": csv_base64,
                "filetypecode": 0,  # CSV
                "processingstatus": 1,  # Not Started
                "recordsownerid_systemuser@odata.bind": f"/systemusers({self.integration.credentials.user_id})"
                if hasattr(self.integration.credentials, "user_id")
                else None,
                "importid@odata.bind": f"/imports({import_id})",
                "targetentityname": entity,
                "headerrow": "Yes",
                "datadelimiter": ",",
                "fielddelimiter": ",",
                "isfirstrowheader": True,
                "processcode": 1,  # Process
                "usesystemmap": False,
            }

            # Remove null values
            import_file_data = {
                k: v for k, v in import_file_data.items() if v is not None
            }

            file_url = (
                f"{self.integration.credentials.resource}/api/data/v9.2/importfiles"
            )

            async with await self.integration._make_request(
                "POST", file_url, json=import_file_data
            ) as resp:
                if resp.status != 201:
                    error_text = await resp.text()
                    raise Exception(f"Failed to create import file: {error_text}")

                file_id = (
                    resp.headers.get("OData-EntityId", "").split("(")[1].rstrip(")")
                )

            if progress_cb:
                progress_cb(0.4, "CSV file uploaded...")

            # Step 5: Start the import using ParseImport action
            parse_url = (
                f"{self.integration.credentials.resource}/api/data/v9.2/ParseImport"
            )
            parse_data = {"ImportId": import_id}

            async with await self.integration._make_request(
                "POST", parse_url, json=parse_data
            ) as resp:
                if resp.status not in [200, 204]:
                    error_text = await resp.text()
                    raise Exception(f"Failed to start import: {error_text}")

            if progress_cb:
                progress_cb(0.5, "Import started, monitoring progress...")

            # Step 6: Poll for completion on the correct endpoint
            result = await self._poll_import_status(import_id, file_id, progress_cb)

            return result

        except Exception as e:
            logger.error(f"Import failed: {str(e)}")
            return ImportJobResult(
                job_id="", status="failed", error_details=[{"error": str(e)}]
            )

    async def _poll_import_status(
        self,
        import_id: str,
        file_id: str,
        progress_cb: Optional[Callable[[float, str], None]] = None,
    ) -> ImportJobResult:
        """Poll import status until completion - FIXED to use importjobs endpoint"""

        # Poll the import job, not the file
        poll_url = f"{self.integration.credentials.resource}/api/data/v9.2/importjobs"
        params = {
            "$filter": f"importid eq {import_id}",
            "$select": "completedon,progress,data",
            "$orderby": "createdon desc",
            "$top": 1,
        }

        max_polls = 300  # 5 minutes with 1 second intervals
        poll_count = 0

        while poll_count < max_polls:
            async with await self.integration._make_request(
                "GET", poll_url, params=params
            ) as resp:
                if resp.status == 200:
                    data = await resp.json()
                    jobs = data.get("value", [])

                    if jobs:
                        job = jobs[0]
                        progress = job.get("progress", 0)
                        completed_on = job.get("completedon")
                        job_data = job.get("data", "")

                        if completed_on:  # Job is complete
                            # Parse job data for results
                            success_count = 0
                            failure_count = 0

                            # Try to parse the data field for counts
                            if job_data:
                                try:
                                    # Data might be XML or structured text
                                    if "<" in job_data:
                                        # Simple XML parsing
                                        import xml.etree.ElementTree as ET

                                        root = ET.fromstring(job_data)
                                        success_count = int(
                                            root.findtext(".//SuccessCount", "0")
                                        )
                                        failure_count = int(
                                            root.findtext(".//FailureCount", "0")
                                        )
                                except:
                                    logger.warning(
                                        "Could not parse job data for counts"
                                    )

                            return ImportJobResult(
                                job_id=import_id,
                                status="completed" if failure_count == 0 else "partial",
                                records_processed=success_count + failure_count,
                                records_succeeded=success_count,
                                records_failed=failure_count,
                                completion_time=datetime.utcnow(),
                            )

                        # Update progress
                        if progress_cb and progress > 0:
                            progress_pct = min(
                                0.9, 0.5 + (progress / 200.0)
                            )  # Map 0-100 to 0.5-0.9
                            progress_cb(progress_pct, f"Import progress: {progress}%")

            await asyncio.sleep(1)
            poll_count += 1

        return ImportJobResult(
            job_id=import_id,
            status="timeout",
            error_details=[{"error": "Import timed out"}],
        )

    async def export_to_df(
        self,
        entity: str,
        fields: List[str],
        filter_xml: Optional[str] = None,
        progress_cb: Optional[Callable[[float, str], None]] = None,
    ) -> pd.DataFrame:
        """
        Export data using FetchXML with paging

        Args:
            entity: Logical name of the entity
            fields: List of fields to export
            filter_xml: Optional FetchXML filter conditions
            progress_cb: Progress callback function

        Returns:
            DataFrame with exported data
        """
        # Build FetchXML query
        fetch_xml = self._build_fetch_xml(entity, fields, filter_xml)

        all_records = []
        page_number = 1
        paging_cookie = None
        more_records = True

        if progress_cb:
            progress_cb(0.0, f"Exporting {entity} data...")

        # Get proper entity set name
        entity_set = self.integration._get_entity_set_name(entity)

        while more_records:
            # Add paging info to FetchXML
            paged_fetch = self._add_paging_to_fetch(
                fetch_xml, page_number, self.page_size, paging_cookie
            )

            # Execute query
            query_url = (
                f"{self.integration.credentials.resource}/api/data/v9.2/{entity_set}"
            )
            headers = self.integration._get_headers()
            headers["Prefer"] = (
                "odata.include-annotations=Microsoft.Dynamics.CRM.fetchxmlpagingcookie"
            )

            params = {"fetchXml": paged_fetch}

            async with await self.integration._make_request(
                "GET", query_url, headers=headers, params=params
            ) as resp:
                if resp.status == 200:
                    data = await resp.json()
                    records = data.get("value", [])
                    all_records.extend(records)

                    # Check for more pages
                    fetch_paging_cookie = data.get(
                        "@Microsoft.Dynamics.CRM.fetchxmlpagingcookie"
                    )
                    if fetch_paging_cookie:
                        # Extract paging cookie from annotation
                        import xml.etree.ElementTree as ET

                        root = ET.fromstring(f"<root>{fetch_paging_cookie}</root>")
                        paging_cookie = root.get("pagingcookie")
                        more_records = root.get("morerecords") == "true"
                        page_number += 1
                    else:
                        more_records = False

                    # Update progress
                    if progress_cb:
                        progress = min(0.9, page_number * 0.1)
                        progress_cb(progress, f"Exported {len(all_records)} records...")
                else:
                    error_text = await resp.text()
                    raise Exception(f"Export failed: {error_text}")

        # Convert to DataFrame
        if all_records:
            df = pd.DataFrame(all_records)
            # Keep only requested fields
            df = df[[f for f in fields if f in df.columns]]
        else:
            df = pd.DataFrame(columns=fields)

        if progress_cb:
            progress_cb(1.0, f"Export complete: {len(df)} records")

        return df

    def _build_fetch_xml(
        self, entity: str, fields: List[str], filter_xml: Optional[str] = None
    ) -> str:
        """Build FetchXML query"""
        attributes = "\n".join([f'<attribute name="{field}" />' for field in fields])

        filter_section = ""
        if filter_xml:
            filter_section = f"<filter>\n{filter_xml}\n</filter>"

        fetch_xml = f"""
        <fetch version="1.0" output-format="xml-platform" mapping="logical" distinct="false">
            <entity name="{entity}">
                {attributes}
                {filter_section}
            </entity>
        </fetch>
        """

        return fetch_xml.strip()

    def _add_paging_to_fetch(
        self, fetch_xml: str, page: int, count: int, paging_cookie: Optional[str] = None
    ) -> str:
        """Add paging attributes to FetchXML"""
        root = ET.fromstring(fetch_xml)
        root.set("page", str(page))
        root.set("count", str(count))

        if paging_cookie:
            # Decode and add paging cookie
            root.set("paging-cookie", paging_cookie)

        return ET.tostring(root, encoding="unicode")

    def _is_valid_guid(self, value: str) -> bool:
        """Check if value is a valid GUID"""
        guid_pattern = re.compile(
            r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"
        )
        return bool(guid_pattern.match(str(value)))

    async def execute_bulk_upsert(
        self,
        entity: str,
        records: List[Dict[str, Any]],
        key_field: str,
        progress_cb: Optional[Callable[[float, str], None]] = None,
    ) -> Dict[str, Any]:
        """
        Execute bulk upsert using batch API - FIXED URL construction

        Args:
            entity: Entity logical name
            records: List of records to upsert
            key_field: Field to use as key for matching
            progress_cb: Progress callback

        Returns:
            Dict with success/failure counts
        """
        results = {"total": len(records), "succeeded": 0, "failed": 0, "errors": []}

        # Get proper entity set name
        entity_set = self.integration._get_entity_set_name(entity)

        # Process in batches of 100 (Dynamics batch limit)
        batch_size = 100
        batches = [
            records[i : i + batch_size] for i in range(0, len(records), batch_size)
        ]

        for batch_idx, batch in enumerate(batches):
            if progress_cb:
                progress = batch_idx / len(batches)
                progress_cb(
                    progress, f"Processing batch {batch_idx + 1} of {len(batches)}"
                )

            batch_requests = []

            for record in batch:
                # Build upsert request with proper URL construction
                if key_field in record and record[key_field]:
                    key_value = record[key_field]

                    # Check if key is a GUID or alternate key
                    if self._is_valid_guid(str(key_value)):
                        # GUID-based update
                        request = {
                            "method": "PATCH",
                            "url": f"{entity_set}({key_value})",
                            "headers": {
                                "Content-Type": "application/json",
                                "If-Match": "*",  # Update regardless of version
                            },
                            "body": {k: v for k, v in record.items() if k != key_field},
                        }
                    else:
                        # Alternate key syntax: entityset(keyfield='value')
                        # Properly escape the key value
                        escaped_value = str(key_value).replace("'", "''")
                        request = {
                            "method": "PATCH",
                            "url": f"{entity_set}({key_field}='{escaped_value}')",
                            "headers": {
                                "Content-Type": "application/json",
                                "If-Match": "*",
                            },
                            "body": {k: v for k, v in record.items() if k != key_field},
                        }
                else:
                    # Create new record
                    request = {
                        "method": "POST",
                        "url": f"{entity_set}",
                        "headers": {"Content-Type": "application/json"},
                        "body": record,
                    }

                batch_requests.append(request)

            # Execute batch
            batch_results = await self.integration.execute_batch(batch_requests)

            # Process results
            for idx, result in enumerate(batch_results):
                if result.get("status", 0) in [200, 201, 204]:
                    results["succeeded"] += 1
                else:
                    results["failed"] += 1
                    results["errors"].append(
                        {
                            "record": batch[idx],
                            "error": result.get("error", "Unknown error"),
                            "status": result.get("status", 0),
                        }
                    )

        if progress_cb:
            progress_cb(1.0, "Bulk upsert complete")

        return results
