"""
HubSpot Bulk API module for high-volume import/export operations.
Implements CSV imports and data exports with progress tracking.
"""

import asyncio
import csv
import io
import json
import logging
import os
import tempfile
from dataclasses import dataclass
from typing import Any, Dict, List, Optional, Tuple
from urllib.parse import urlparse

import pandas as pd
import aiofiles
from aiohttp import FormData

from .core import HubSpotIntegration

logger = logging.getLogger(__name__)


@dataclass
class ImportJobResult:
    """Result of a HubSpot import job."""

    job_id: str
    status: str  # PENDING, PROCESSING, DONE, FAILED
    records_processed: int = 0
    records_created: int = 0
    records_updated: int = 0
    failed_count: int = 0
    error_file_url: Optional[str] = None
    errors: List[Dict[str, Any]] = None

    def __post_init__(self):
        if self.errors is None:
            self.errors = []


@dataclass
class ExportJobResult:
    """Result of a HubSpot export job."""

    job_id: str
    status: str
    file_url: Optional[str] = None
    record_count: int = 0
    error_message: Optional[str] = None


class HubSpotBulkAPI:
    """
    Handles bulk operations using HubSpot's Import and Export APIs.
    """

    def __init__(self, integration: HubSpotIntegration):
        self.integration = integration
        self.max_csv_size_mb = 500  # Stay under proxy limits
        self.max_poll_attempts = 60  # 5 minutes with 5-second intervals
        self.poll_interval = 5

    async def _get_object_type_id(self, object_name: str) -> str:
        """Get the object type ID for a given object name."""
        standard_ids = {
            "contacts": "0-1",
            "companies": "0-2",
            "deals": "0-3",
            "tickets": "0-5",
        }

        if object_name in standard_ids:
            return standard_ids[object_name]

        # Custom objects need schema scope
        result, status = await self.integration._execute_api_request(
            "GET", f"/crm/v3/schemas/{object_name}"
        )

        if status == 403:
            raise ValueError(
                f"Missing 'crm.schemas.{object_name}.read' scope for custom object '{object_name}'"
            )
        elif status != 200 or not result:
            raise ValueError(f"Unable to fetch schema for object '{object_name}'")

        return result.get("objectTypeId", "0-1")

    async def import_csv(
        self,
        object_name: str,
        df: pd.DataFrame,
        mapping: Dict[str, str],
        import_name: str = "FuzzyMatcher Import",
        progress_callback: Optional[callable] = None,
    ) -> ImportJobResult:
        """
        Import data using HubSpot's CSV import API.

        Args:
            object_name: HubSpot object type (contacts, companies, etc.)
            df: DataFrame with data to import
            mapping: Column mapping {df_column: hubspot_property}
            import_name: Name for the import job
            progress_callback: Callback for progress updates

        Returns:
            ImportJobResult with status and counts
        """
        if progress_callback:
            progress_callback("Preparing CSV for import", 0)

        # Apply column mapping
        mapped_df = df.rename(columns=mapping)

        # Check size and split if needed
        csv_chunks = await self._prepare_csv_chunks(mapped_df, progress_callback)

        results = []

        # Get the correct object type ID once for all chunks
        object_type_id = await self._get_object_type_id(object_name)

        for i, (csv_data, row_count) in enumerate(csv_chunks):
            chunk_name = (
                f"{import_name} (Part {i+1})" if len(csv_chunks) > 1 else import_name
            )

            if progress_callback:
                base_progress = (i / len(csv_chunks)) * 90
                progress_callback(f"Uploading {chunk_name}", base_progress)

            # Create import request
            import_request = {
                "name": chunk_name,
                "files": [
                    {
                        "fileName": f"import_{object_name}_{i}.csv",
                        "fileImportPage": {
                            "hasHeader": True,
                            "columnMappings": [
                                {
                                    "columnName": col,
                                    "propertyName": col,
                                    "columnObjectTypeId": object_type_id,  # Use dynamic ID
                                }
                                for col in mapped_df.columns
                            ],
                        },
                    }
                ],
                "importOperations": {
                    object_name: "CREATE_AND_UPDATE"  # or "CREATE_ONLY", "UPDATE_ONLY"
                },
            }

            # Start import job
            job_id = await self._start_import_job(import_request, csv_data)

            if not job_id:
                results.append(
                    ImportJobResult(
                        job_id="",
                        status="FAILED",
                        error_message="Failed to start import job",
                    )
                )
                continue

            # Poll for completion
            result = await self._poll_import_job(
                job_id,
                row_count,
                lambda msg, pct: progress_callback(
                    msg, base_progress + (pct / len(csv_chunks) * 0.9)
                )
                if progress_callback
                else None,
            )

            results.append(result)

        # Combine results if multiple chunks
        if len(results) == 1:
            return results[0]
        else:
            combined = ImportJobResult(
                job_id=",".join(r.job_id for r in results),
                status="DONE"
                if all(r.status == "DONE" for r in results)
                else "PARTIAL",
                records_processed=sum(r.records_processed for r in results),
                records_created=sum(r.records_created for r in results),
                records_updated=sum(r.records_updated for r in results),
                failed_count=sum(r.failed_count for r in results),
                errors=[err for r in results for err in r.errors],
            )

            if progress_callback:
                progress_callback("Import completed", 100)

            return combined

    async def export_to_df(
        self,
        object_name: str,
        properties: List[str],
        filters: Optional[Dict[str, Any]] = None,
        progress_callback: Optional[callable] = None,
    ) -> pd.DataFrame:
        """
        Export HubSpot data to DataFrame using the Export API.

        Args:
            object_name: HubSpot object type
            properties: List of properties to export
            filters: Optional filters for the export
            progress_callback: Progress callback

        Returns:
            DataFrame with exported data
        """
        if progress_callback:
            progress_callback("Starting export", 0)

        # Create export request
        export_request = {
            "objectType": object_name,
            "properties": properties,
            "format": "CSV",
            "language": "EN",
        }

        if filters:
            export_request["filters"] = filters

        # Start export job
        job_id = await self._start_export_job(export_request)

        if not job_id:
            raise Exception("Failed to start export job")

        # Poll for completion
        result = await self._poll_export_job(job_id, progress_callback)

        if result.status != "DONE" or not result.file_url:
            raise Exception(f"Export failed: {result.error_message}")

        # Download the exported file
        if progress_callback:
            progress_callback("Downloading export file", 90)

        csv_data = await self._download_export_file(result.file_url)

        # Parse to DataFrame
        df = pd.read_csv(io.StringIO(csv_data))

        if progress_callback:
            progress_callback(f"Export completed: {len(df)} records", 100)

        return df

    async def _prepare_csv_chunks(
        self, df: pd.DataFrame, progress_callback: Optional[callable] = None
    ) -> List[Tuple[str, int]]:
        """
        Split DataFrame into CSV chunks under size limit.
        Returns list of (csv_string, row_count) tuples.
        """
        # First, try as single CSV
        csv_buffer = io.StringIO()
        df.to_csv(csv_buffer, index=False)
        csv_data = csv_buffer.getvalue()

        size_mb = len(csv_data.encode("utf-8")) / (1024 * 1024)

        if size_mb < self.max_csv_size_mb:
            return [(csv_data, len(df))]

        # Need to chunk
        logger.info(f"CSV is {size_mb:.1f}MB, splitting into chunks")

        chunks = []
        chunk_size = int(
            len(df) * (self.max_csv_size_mb / size_mb) * 0.9
        )  # 90% to be safe

        for i in range(0, len(df), chunk_size):
            chunk_df = df.iloc[i : i + chunk_size]
            csv_buffer = io.StringIO()
            chunk_df.to_csv(csv_buffer, index=False)
            chunks.append((csv_buffer.getvalue(), len(chunk_df)))

            if progress_callback:
                progress = (i / len(df)) * 10  # First 10% is chunking
                progress_callback(f"Preparing chunk {len(chunks)}", progress)

        return chunks

    async def _start_import_job(
        self, import_request: Dict[str, Any], csv_data: str
    ) -> Optional[str]:
        """Start a HubSpot import job and return job ID."""
        try:
            # Create multipart form data
            form = FormData()

            # Add import request JSON
            form.add_field(
                "importRequest",
                json.dumps(import_request),
                content_type="application/json",
            )

            # Add CSV file
            form.add_field(
                "files",
                csv_data.encode("utf-8"),
                filename="import.csv",
                content_type="text/csv",
            )

            # Start import
            result, status = await self.integration._execute_api_request(
                "POST", "/crm/v3/imports", data=form
            )

            if status == 200 and result:
                return result.get("id")
            else:
                logger.error(f"Failed to start import: {result}")
                return None

        except Exception as e:
            logger.error(f"Error starting import job: {e}")
            return None

    async def _poll_import_job(
        self,
        job_id: str,
        expected_rows: int,
        progress_callback: Optional[callable] = None,
    ) -> ImportJobResult:
        """Poll import job status until completion."""
        attempts = 0

        while attempts < self.max_poll_attempts:
            result, status = await self.integration._execute_api_request(
                "GET", f"/crm/v3/imports/{job_id}"
            )

            if status != 200:
                logger.error(f"Error polling import job: {result}")
                return ImportJobResult(
                    job_id=job_id, status="FAILED", error_message=str(result)
                )

            import_status = result.get("state", "UNKNOWN")

            # Update progress
            if progress_callback and "metadata" in result:
                counters = result["metadata"].get("counters", {})
                processed = counters.get("TOTAL_ROWS", 0)
                progress_pct = (
                    min((processed / expected_rows) * 100, 99)
                    if expected_rows > 0
                    else 0
                )
                progress_callback(
                    f"Processing import: {processed}/{expected_rows} rows", progress_pct
                )

            # Check if done
            if import_status in ["DONE", "FAILED", "CANCELED"]:
                return self._parse_import_result(job_id, result)

            attempts += 1
            await asyncio.sleep(self.poll_interval)

        # Timeout
        return ImportJobResult(
            job_id=job_id, status="TIMEOUT", error_message="Import job timed out"
        )

    def _parse_import_result(
        self, job_id: str, result: Dict[str, Any]
    ) -> ImportJobResult:
        """Parse import job result from API response."""
        metadata = result.get("metadata", {})
        counters = metadata.get("counters", {})

        import_result = ImportJobResult(
            job_id=job_id,
            status=result.get("state", "UNKNOWN"),
            records_processed=counters.get("TOTAL_ROWS", 0),
            records_created=counters.get("CREATED", 0),
            records_updated=counters.get("UPDATED", 0),
            failed_count=counters.get("FAILED", 0),
        )

        # Get error file if present
        if "errors" in result:
            for error in result["errors"]:
                if error.get("errorType") == "IMPORT_FAILURE_RECORD":
                    import_result.error_file_url = error.get("link")

        return import_result

    async def _start_export_job(self, export_request: Dict[str, Any]) -> Optional[str]:
        """Start a HubSpot export job and return job ID."""
        try:
            result, status = await self.integration._execute_api_request(
                "POST", "/crm/v3/exports", json=export_request
            )

            if status == 200 and result:
                return result.get("id")
            else:
                logger.error(f"Failed to start export: {result}")
                return None

        except Exception as e:
            logger.error(f"Error starting export job: {e}")
            return None

    async def _poll_export_job(
        self, job_id: str, progress_callback: Optional[callable] = None
    ) -> ExportJobResult:
        """Poll export job status until completion."""
        attempts = 0

        while attempts < self.max_poll_attempts:
            result, status = await self.integration._execute_api_request(
                "GET", f"/crm/v3/exports/{job_id}"
            )

            if status != 200:
                logger.error(f"Error polling export job: {result}")
                return ExportJobResult(
                    job_id=job_id, status="FAILED", error_message=str(result)
                )

            export_status = result.get("state", "UNKNOWN")

            # Update progress
            if progress_callback:
                # Estimate progress based on time
                progress_pct = min(attempts * 5, 90)  # Cap at 90%
                progress_callback(f"Export job {export_status}", progress_pct)

            # Check if done
            if export_status == "DONE":
                return ExportJobResult(
                    job_id=job_id,
                    status=export_status,
                    file_url=result.get("fileUrl"),
                    record_count=result.get("metadata", {}).get("totalRows", 0),
                )
            elif export_status in ["FAILED", "CANCELED"]:
                return ExportJobResult(
                    job_id=job_id,
                    status=export_status,
                    error_message=result.get("message", "Export failed"),
                )

            attempts += 1
            await asyncio.sleep(self.poll_interval)

        # Timeout
        return ExportJobResult(
            job_id=job_id, status="TIMEOUT", error_message="Export job timed out"
        )

    async def _download_export_file(self, file_url: str) -> str:
        """Download exported CSV file from HubSpot."""
        # Use integration's session to download
        async with self.integration.session.get(file_url) as response:
            if response.status == 200:
                return await response.text()
            else:
                raise Exception(f"Failed to download export file: {response.status}")

    async def batch_merge_duplicates(
        self,
        object_name: str,
        duplicate_pairs: List[Tuple[str, str]],
        progress_callback: Optional[callable] = None,
    ) -> Dict[str, Any]:
        """
        Merge duplicate records in batches.

        Args:
            object_name: HubSpot object type
            duplicate_pairs: List of (primary_id, duplicate_id) tuples
            progress_callback: Progress callback

        Returns:
            Dict with merge results
        """
        total_merged = 0
        errors = []

        for i, (primary_id, duplicate_id) in enumerate(duplicate_pairs):
            if progress_callback:
                progress = (i / len(duplicate_pairs)) * 100
                progress_callback(f"Merging {i+1}/{len(duplicate_pairs)}", progress)

            # HubSpot merge endpoint
            endpoint = f"/crm/v3/objects/{object_name}/merge"
            payload = {"primaryObjectId": primary_id, "objectIdToMerge": duplicate_id}

            result, status = await self.integration._execute_api_request(
                "POST", endpoint, json=payload
            )

            if status == 200:
                total_merged += 1
            else:
                errors.append(
                    {
                        "primary_id": primary_id,
                        "duplicate_id": duplicate_id,
                        "error": result,
                    }
                )

        if progress_callback:
            progress_callback("Merge completed", 100)

        return {"merged": total_merged, "failed": len(errors), "errors": errors}

    async def batch_associate(
        self,
        associations: List[Dict[str, str]],
        progress_callback: Optional[callable] = None,
    ) -> Dict[str, Any]:
        """
        Create associations in batches.

        Args:
            associations: List of dicts with keys:
                - from_object, from_id, to_object, to_id, association_type
            progress_callback: Progress callback

        Returns:
            Dict with association results
        """
        # HubSpot batch association endpoint supports up to 100 at a time
        batch_size = 100
        total_created = 0
        errors = []

        for i in range(0, len(associations), batch_size):
            batch = associations[i : i + batch_size]

            if progress_callback:
                progress = (i / len(associations)) * 100
                progress_callback(
                    f"Creating associations batch {i//batch_size + 1}", progress
                )

            # Group by from_object and to_object for efficiency
            grouped = {}
            for assoc in batch:
                key = (
                    assoc["from_object"],
                    assoc["to_object"],
                    assoc.get("association_type", "default"),
                )
                if key not in grouped:
                    grouped[key] = []
                grouped[key].append(
                    {"from": {"id": assoc["from_id"]}, "to": {"id": assoc["to_id"]}}
                )

            # Create associations for each group
            for (from_obj, to_obj, assoc_type), items in grouped.items():
                endpoint = f"/crm/v3/associations/{from_obj}/{to_obj}/batch/create"

                # Build payload with type at correct level
                payload = {
                    "inputs": [
                        {"from": item["from"], "to": item["to"], "type": assoc_type}
                        for item in items
                    ]
                }
                result, status = await self.integration._execute_api_request(
                    "POST", endpoint, json=payload
                )

                if status == 201:
                    total_created += len(items)
                else:
                    errors.append({"batch": f"{from_obj}->{to_obj}", "error": result})

        if progress_callback:
            progress_callback("Associations completed", 100)

        return {"created": total_created, "failed": len(errors), "errors": errors}
