"""
HubSpot workflow engine for dedupe, association, and other business logic operations.
"""

import logging
from dataclasses import dataclass, field
from datetime import datetime
from typing import Any, Dict, List, Optional, Tuple

import pandas as pd

from .core import HubSpotIntegration
from .bulk_api import HubSpotBulkAPI
from .data_validation import HubSpotDataValidator

logger = logging.getLogger(__name__)


@dataclass
class WorkflowConfig:
    """Base configuration for workflows."""

    name: str
    object_name: str
    enabled: bool = True
    dry_run: bool = False
    batch_size: int = 100
    metadata: Dict[str, Any] = field(default_factory=dict)


@dataclass
class DedupeWorkflowConfig(WorkflowConfig):
    """Configuration for deduplication workflow."""

    match_fields: List[str] = field(default_factory=list)
    confidence_threshold: float = 0.85
    merge_strategy: str = "newest"  # newest, oldest, most_complete, custom
    preserve_fields: List[str] = field(default_factory=list)


@dataclass
class AssociationWorkflowConfig(WorkflowConfig):
    """Configuration for association workflow."""

    from_object: str = "contacts"
    to_object: str = "companies"
    association_type: str = "default"
    match_field_pairs: List[Tuple[str, str]] = field(default_factory=list)
    confidence_threshold: float = 0.80
    create_missing: bool = False


@dataclass
class WorkflowResult:
    """Result of workflow execution."""

    workflow_name: str
    status: str  # 'success', 'partial', 'failed'
    start_time: datetime
    end_time: Optional[datetime] = None
    records_processed: int = 0
    records_modified: int = 0
    records_failed: int = 0
    api_calls_made: int = 0
    errors: List[Dict[str, Any]] = field(default_factory=list)
    metadata: Dict[str, Any] = field(default_factory=dict)

    def complete(self):
        """Mark workflow as complete."""
        self.end_time = datetime.now()
        if self.records_failed == 0:
            self.status = "success"
        elif self.records_modified > 0:
            self.status = "partial"
        else:
            self.status = "failed"


@dataclass
class WorkflowPreview:
    """Preview of workflow actions before execution."""

    total_records: int
    estimated_api_calls: int
    estimated_duration_seconds: float
    actions: List[Dict[str, Any]]
    validation_issues: List[Any] = field(default_factory=list)
    warnings: List[str] = field(default_factory=list)


class HubSpotWorkflowEngine:
    """
    Engine for executing HubSpot workflows with preview, backup, and audit support.
    """

    def __init__(
        self,
        integration: HubSpotIntegration,
        bulk_api: Optional[HubSpotBulkAPI] = None,
        validator: Optional[HubSpotDataValidator] = None,
        audit_logger: Optional[Any] = None,
    ):
        self.integration = integration
        self.bulk_api = bulk_api or HubSpotBulkAPI(integration)
        self.validator = validator or HubSpotDataValidator(integration)
        self.audit_logger = audit_logger

    async def preview_workflow(
        self, config: WorkflowConfig, data: pd.DataFrame
    ) -> WorkflowPreview:
        """
        Preview workflow actions without executing.

        Args:
            config: Workflow configuration
            data: Input data

        Returns:
            WorkflowPreview with estimated impact
        """
        if isinstance(config, DedupeWorkflowConfig):
            return await self._preview_dedupe_workflow(config, data)
        elif isinstance(config, AssociationWorkflowConfig):
            return await self._preview_association_workflow(config, data)
        else:
            raise ValueError(f"Unknown workflow type: {type(config)}")

    async def execute_workflow(
        self,
        config: WorkflowConfig,
        data: pd.DataFrame,
        progress_callback: Optional[callable] = None,
    ) -> WorkflowResult:
        """
        Execute a workflow with progress tracking.

        Args:
            config: Workflow configuration
            data: Input data
            progress_callback: Progress callback function

        Returns:
            WorkflowResult with execution details
        """
        result = WorkflowResult(
            workflow_name=config.name, status="running", start_time=datetime.now()
        )

        try:
            # Audit workflow start
            if self.audit_logger:
                await self.audit_logger.audit_workflow_start(
                    workflow_name=config.name,
                    config=config.__dict__,
                    input_count=len(data),
                )

            # Execute based on workflow type
            if isinstance(config, DedupeWorkflowConfig):
                await self._execute_dedupe_workflow(
                    config, data, result, progress_callback
                )
            elif isinstance(config, AssociationWorkflowConfig):
                await self._execute_association_workflow(
                    config, data, result, progress_callback
                )
            else:
                raise ValueError(f"Unknown workflow type: {type(config)}")

        except Exception as e:
            logger.error(f"Workflow execution error: {e}")
            result.errors.append({"type": "execution_error", "message": str(e)})

        finally:
            result.complete()

            # Audit workflow completion
            if self.audit_logger:
                await self.audit_logger.audit_workflow_completion(
                    workflow_name=config.name, result=result.__dict__
                )

        return result

    async def _preview_dedupe_workflow(
        self, config: DedupeWorkflowConfig, data: pd.DataFrame
    ) -> WorkflowPreview:
        """Preview deduplication workflow."""
        preview = WorkflowPreview(
            total_records=len(data),
            estimated_api_calls=0,
            estimated_duration_seconds=0,
            actions=[],
        )

        # Validate data
        validation_result = await self.validator.validate_dataframe(
            data, config.object_name, operation="UPDATE_ONLY"
        )

        if not validation_result.valid:
            preview.validation_issues = validation_result.issues
            preview.warnings.append(
                f"Found {validation_result.error_count} validation errors"
            )

        # Find duplicates
        duplicate_groups = self._find_duplicate_groups(data, config.match_fields)

        # Estimate actions
        merge_count = sum(
            len(group) - 1 for group in duplicate_groups if len(group) > 1
        )
        preview.actions.append(
            {
                "action": "merge_duplicates",
                "count": merge_count,
                "details": f"Found {len(duplicate_groups)} duplicate groups",
            }
        )

        # Estimate API calls
        preview.estimated_api_calls = merge_count  # One merge call per duplicate
        preview.estimated_duration_seconds = merge_count * 0.5  # ~0.5s per merge

        return preview

    async def _preview_association_workflow(
        self, config: AssociationWorkflowConfig, data: pd.DataFrame
    ) -> WorkflowPreview:
        """Preview association workflow."""
        preview = WorkflowPreview(
            total_records=len(data),
            estimated_api_calls=0,
            estimated_duration_seconds=0,
            actions=[],
        )

        # Check required columns
        required_cols = ["from_id", "to_id", "confidence_score"]
        missing_cols = [col for col in required_cols if col not in data.columns]
        if missing_cols:
            preview.warnings.append(f"Missing required columns: {missing_cols}")
            return preview

        # Filter by confidence threshold
        high_confidence = data[data["confidence_score"] >= config.confidence_threshold]

        preview.actions.append(
            {
                "action": "create_associations",
                "count": len(high_confidence),
                "details": f"Associations above {config.confidence_threshold} confidence",
            }
        )

        # Estimate API calls (batch endpoint handles 100 at a time)
        batch_count = (len(high_confidence) + 99) // 100
        preview.estimated_api_calls = batch_count
        preview.estimated_duration_seconds = batch_count * 2  # ~2s per batch

        return preview

    async def _execute_dedupe_workflow(
        self,
        config: DedupeWorkflowConfig,
        data: pd.DataFrame,
        result: WorkflowResult,
        progress_callback: Optional[callable] = None,
    ):
        """Execute deduplication workflow."""
        if progress_callback:
            progress_callback("Finding duplicates", 0)

        # Find duplicate groups
        duplicate_groups = self._find_duplicate_groups(data, config.match_fields)
        result.metadata["duplicate_groups_found"] = len(duplicate_groups)

        # Prepare merge pairs
        merge_pairs = []
        for group in duplicate_groups:
            if len(group) < 2:
                continue

            # Determine primary record based on strategy
            primary_idx = self._select_primary_record(
                data, group, config.merge_strategy
            )
            primary_id = data.loc[primary_idx, "id"]

            # Add merge pairs for other records in group
            for idx in group:
                if idx != primary_idx:
                    duplicate_id = data.loc[idx, "id"]
                    merge_pairs.append((str(primary_id), str(duplicate_id)))

        result.metadata["merge_pairs_count"] = len(merge_pairs)

        if config.dry_run:
            result.records_processed = len(merge_pairs)
            result.metadata["dry_run"] = True
            if progress_callback:
                progress_callback("Dry run completed", 100)
            return

        # Execute merges
        if progress_callback:
            progress_callback(f"Merging {len(merge_pairs)} duplicates", 10)

        merge_result = await self.bulk_api.batch_merge_duplicates(
            config.object_name,
            merge_pairs,
            lambda msg, pct: progress_callback(msg, 10 + pct * 0.9)
            if progress_callback
            else None,
        )

        result.records_processed = len(merge_pairs)
        result.records_modified = merge_result["merged"]
        result.records_failed = merge_result["failed"]
        result.errors.extend(merge_result.get("errors", []))
        result.api_calls_made = len(merge_pairs)  # Each merge is one API call

    async def _execute_association_workflow(
        self,
        config: AssociationWorkflowConfig,
        data: pd.DataFrame,
        result: WorkflowResult,
        progress_callback: Optional[callable] = None,
    ):
        """Execute association workflow."""
        if progress_callback:
            progress_callback("Preparing associations", 0)

        # Filter by confidence threshold
        high_confidence = data[data["confidence_score"] >= config.confidence_threshold]
        result.metadata["high_confidence_matches"] = len(high_confidence)

        # Prepare association requests
        associations = []
        for _, row in high_confidence.iterrows():
            associations.append(
                {
                    "from_object": config.from_object,
                    "from_id": str(row["from_id"]),
                    "to_object": config.to_object,
                    "to_id": str(row["to_id"]),
                    "association_type": config.association_type,
                }
            )

        result.records_processed = len(associations)

        if config.dry_run:
            result.metadata["dry_run"] = True
            if progress_callback:
                progress_callback("Dry run completed", 100)
            return

        # Execute associations
        if progress_callback:
            progress_callback(f"Creating {len(associations)} associations", 10)

        assoc_result = await self.bulk_api.batch_associate(
            associations,
            lambda msg, pct: progress_callback(msg, 10 + pct * 0.9)
            if progress_callback
            else None,
        )

        result.records_modified = assoc_result["created"]
        result.records_failed = assoc_result["failed"]
        result.errors.extend(assoc_result.get("errors", []))
        result.api_calls_made = (len(associations) + 99) // 100  # Batch calls

    def _find_duplicate_groups(
        self, data: pd.DataFrame, match_fields: List[str]
    ) -> List[List[int]]:
        """Find groups of duplicate records based on match fields."""
        if not match_fields:
            return []

        # Create composite key from match fields
        data["_match_key"] = (
            data[match_fields]
            .fillna("")
            .apply(
                lambda row: "|".join(str(val).lower().strip() for val in row), axis=1
            )
        )

        # Group by match key
        groups = []
        for match_key, group in data.groupby("_match_key"):
            # Skip empty keys (all fields were null/empty)
            if match_key and match_key.strip("|") and len(group) > 1:
                groups.append(group.index.tolist())

        # Clean up
        data.drop("_match_key", axis=1, inplace=True)

        return groups

    def _select_primary_record(
        self, data: pd.DataFrame, group_indices: List[int], strategy: str
    ) -> int:
        """Select primary record from duplicate group based on strategy."""
        group_data = data.loc[group_indices]

        if strategy == "newest":
            # Select record with latest modified date
            if "lastmodifieddate" in group_data.columns:
                return group_data["lastmodifieddate"].idxmax()
            elif "createdate" in group_data.columns:
                return group_data["createdate"].idxmax()

        elif strategy == "oldest":
            # Select record with earliest create date
            if "createdate" in group_data.columns:
                return group_data["createdate"].idxmin()

        elif strategy == "most_complete":
            # Select record with fewest null values
            null_counts = group_data.isna().sum(axis=1)
            return null_counts.idxmin()

        # Default: return first record
        return group_indices[0]

    async def create_contact_to_company_associations(
        self,
        fuzzy_match_results: pd.DataFrame,
        config: Optional[AssociationWorkflowConfig] = None,
        progress_callback: Optional[callable] = None,
    ) -> WorkflowResult:
        """
        Convenience method for contact-to-company association workflow.

        Args:
            fuzzy_match_results: DataFrame with fuzzy match results
            config: Optional custom configuration
            progress_callback: Progress callback

        Returns:
            WorkflowResult
        """
        if config is None:
            config = AssociationWorkflowConfig(
                name="Contact to Company Association",
                object_name="associations",
                from_object="contacts",
                to_object="companies",
                confidence_threshold=0.80,
            )

        # Ensure required columns exist
        required_columns = {
            "from_id": "contact_id",
            "to_id": "company_id",
            "confidence_score": "match_score",
        }

        # Rename columns if needed
        for required, alternatives in required_columns.items():
            if required not in fuzzy_match_results.columns:
                if alternatives in fuzzy_match_results.columns:
                    fuzzy_match_results[required] = fuzzy_match_results[alternatives]
                else:
                    raise ValueError(f"Missing required column: {required}")

        return await self.execute_workflow(
            config, fuzzy_match_results, progress_callback
        )

    async def deduplicate_records(
        self,
        records: pd.DataFrame,
        object_name: str,
        match_fields: List[str],
        config: Optional[DedupeWorkflowConfig] = None,
        progress_callback: Optional[callable] = None,
    ) -> WorkflowResult:
        """
        Convenience method for deduplication workflow.

        Args:
            records: DataFrame with records to deduplicate
            object_name: HubSpot object type
            match_fields: Fields to match on
            config: Optional custom configuration
            progress_callback: Progress callback

        Returns:
            WorkflowResult
        """
        if config is None:
            config = DedupeWorkflowConfig(
                name=f"{object_name.title()} Deduplication",
                object_name=object_name,
                match_fields=match_fields,
                merge_strategy="most_complete",
            )

        return await self.execute_workflow(config, records, progress_callback)

    async def backup_before_workflow(
        self, object_ids: List[str], object_name: str
    ) -> Dict[str, Any]:
        """
        Create backup of records before workflow execution.

        Args:
            object_ids: List of object IDs to backup
            object_name: HubSpot object type

        Returns:
            Backup metadata
        """
        if not object_ids:
            return {"status": "skipped", "reason": "no_records"}

        backup_data = []

        # Fetch records in batches
        batch_size = 100
        for i in range(0, len(object_ids), batch_size):
            batch_ids = object_ids[i : i + batch_size]

            # Fetch batch
            result, status = await self.integration._execute_api_request(
                "POST",
                f"/crm/v3/objects/{object_name}/batch/read",
                json={
                    # Omit properties to get all fields
                    "inputs": [{"id": obj_id} for obj_id in batch_ids]
                },
            )

            if status == 200 and result:
                backup_data.extend(result.get("results", []))

        # Store backup (in production, this would go to S3 or similar)
        backup_metadata = {
            "timestamp": datetime.now().isoformat(),
            "object_name": object_name,
            "record_count": len(backup_data),
            "data": backup_data,  # In production, store reference not data
        }

        return backup_metadata
