import json
import hashlib
from typing import Dict, List
from datetime import datetime, timedelta
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from sqlalchemy.exc import IntegrityError
import logging

from ..models import FieldMappingPolicy, Integration, IntegrationProvider
from ..services.salesforce_gateway import SalesforceGateway

log = logging.getLogger(__name__)


class FieldMetadataCache:
    """Cache Salesforce describe results"""

    def __init__(self, ttl_seconds=3600):
        self.cache = {}
        self.ttl = ttl_seconds

    async def get_fields(self, sf_client, object_type: str, db_callback=None) -> Dict:
        cache_key = f"{sf_client.instance_url}:{object_type}"
        cached = self.cache.get(cache_key)

        if cached and cached["expires"] > datetime.utcnow():
            return cached["data"]

        # Fetch from Salesforce
        # Accept either gateway or client; both expose describe_object (or alias)
        describe = await (
            sf_client.describe_object(object_type)
            if hasattr(sf_client, "describe_object")
            else sf_client.describe(object_type)
        )

        # Extract essential field info
        fields = {
            f["name"]: {
                "type": f["type"],
                "updateable": f["updateable"],
                "createable": f["createable"],
                "label": f["label"],
                "referenceTo": f.get("referenceTo", []),
                "picklistValues": f.get("picklistValues", []),
            }
            for f in describe["fields"]
        }

        self.cache[cache_key] = {
            "data": fields,
            "expires": datetime.utcnow() + timedelta(seconds=self.ttl),
        }
        return fields


# Global cache instance
field_metadata_cache = FieldMetadataCache()


class PolicyService:
    """Service for managing field mapping policies"""

    def __init__(self, db: AsyncSession):
        self.db = db

    @staticmethod
    def compute_policy_hash(policy: FieldMappingPolicy) -> str:
        """Generate deterministic hash of policy rules"""
        normalized = json.dumps(
            {
                "version": policy.version,
                "object": policy.salesforce_object,
                "rules": policy.matching_rules,
                "writable": sorted(policy.writable_fields),
                "thresholds": {
                    "duplicate": policy.duplicate_threshold,
                    "link": policy.link_threshold,
                },
            },
            sort_keys=True,
        )
        return hashlib.sha256(normalized.encode()).hexdigest()

    async def validate_policy(self, policy: FieldMappingPolicy) -> List[str]:
        """Validate policy against Salesforce schema"""
        errors = []

        # Get Salesforce integration
        integration = await self.db.execute(
            select(Integration).where(
                Integration.account_id == policy.account_id,
                Integration.provider == IntegrationProvider.SALESFORCE,
            )
        )
        sf_integration = integration.scalar_one_or_none()

        if not sf_integration:
            return ["Salesforce not connected"]

        # Get field metadata via gateway (no direct client)
        gw = SalesforceGateway.for_tenant  # type: ignore[attr-defined]
        # NOTE: for_tenant is async; resolve gateway instance
        sf = await SalesforceGateway.for_tenant(policy.account_id, self.db)
        fields = await field_metadata_cache.get_fields(sf, policy.salesforce_object)

        # Validate matching fields
        for rule in policy.matching_rules.get("matching_fields", []):
            field_name = rule["field"]
            if field_name not in fields:
                errors.append(
                    f"Field '{field_name}' does not exist on {policy.salesforce_object}"
                )
                continue

            field_type = fields[field_name]["type"]
            algorithm = rule["algorithm"]

            # Validate algorithm compatibility
            if algorithm == "exact" and field_type not in [
                "string",
                "id",
                "reference",
                "email",
                "phone",
            ]:
                errors.append(
                    f"Field '{field_name}' (type: {field_type}) incompatible with 'exact' matching"
                )
            elif algorithm == "domain" and field_type not in ["email", "url", "string"]:
                errors.append(
                    f"Field '{field_name}' (type: {field_type}) incompatible with 'domain' matching"
                )

        # Validate writable fields
        for field_name in policy.writable_fields:
            if field_name not in fields:
                errors.append(f"Writable field '{field_name}' does not exist")
            elif not fields[field_name]["updateable"]:
                errors.append(f"Field '{field_name}' is not updateable in Salesforce")

        # Store metadata for future reference
        policy.field_metadata = fields
        policy.metadata_updated_at = datetime.utcnow()

        return errors

    async def get_active_policies(self, account_id: str) -> List[FieldMappingPolicy]:
        """Get all active field mapping policies for an account"""
        result = await self.db.execute(
            select(FieldMappingPolicy).where(
                FieldMappingPolicy.account_id == account_id,
                FieldMappingPolicy.is_active == True,
            )
        )
        return result.scalars().all()

    async def create_or_update_policy(
        self, policy_data: Dict, account_id: str, user_id: str
    ) -> FieldMappingPolicy:
        """Create or update a field mapping policy with transaction locking"""

        # Validate thresholds first
        duplicate_threshold = policy_data.get("duplicate_threshold", 0.85)
        link_threshold = policy_data.get("link_threshold", 0.75)

        if not (0.0 <= duplicate_threshold <= 1.0):
            raise ValueError(
                "Invalid threshold: duplicate_threshold must be between 0.0 and 1.0"
            )
        if not (0.0 <= link_threshold <= 1.0):
            raise ValueError(
                "Invalid threshold: link_threshold must be between 0.0 and 1.0"
            )

        try:
            # Start transaction - deactivate old and insert new atomically
            async with self.db.begin():
                # Check for existing active policy with FOR UPDATE to lock
                existing = await self.db.execute(
                    select(FieldMappingPolicy)
                    .where(
                        FieldMappingPolicy.account_id == account_id,
                        FieldMappingPolicy.salesforce_object
                        == policy_data["salesforce_object"],
                        FieldMappingPolicy.is_active == True,
                    )
                    .with_for_update()
                )
                existing_policy = existing.scalar_one_or_none()

                if existing_policy:
                    # Deactivate old version
                    existing_policy.is_active = False

                    # Create new version
                    policy = FieldMappingPolicy(
                        account_id=account_id,
                        salesforce_object=policy_data["salesforce_object"],
                        version=existing_policy.version + 1,
                        matching_rules=policy_data["matching_rules"],
                        writable_fields=policy_data.get("writable_fields", []),
                        duplicate_threshold=duplicate_threshold,
                        link_threshold=link_threshold,
                        created_by=user_id,
                    )
                else:
                    # Create first version
                    policy = FieldMappingPolicy(
                        account_id=account_id,
                        salesforce_object=policy_data["salesforce_object"],
                        version=1,
                        matching_rules=policy_data["matching_rules"],
                        writable_fields=policy_data.get("writable_fields", []),
                        duplicate_threshold=duplicate_threshold,
                        link_threshold=link_threshold,
                        created_by=user_id,
                    )

                # Validate
                errors = await self.validate_policy(policy)
                if errors:
                    raise ValueError(f"Policy validation failed: {'; '.join(errors)}")

                # Compute hash
                policy.policy_hash = self.compute_policy_hash(policy)

                self.db.add(policy)
                await self.db.commit()
                await self.db.refresh(policy)

                return policy

        except IntegrityError:
            await self.db.rollback()
            raise ValueError("A concurrent update occurred. Please retry.")
