"""
Write enrichment results back to Salesforce.

Features:
- FLS validation
- Bulk API for performance
- Audit trail via FM_DataSource__c
- Dry-run mode
- Detailed error reporting
- Archival of enriched data for audit trail and recovery
"""

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

import sqlalchemy as sa
from sqlalchemy.dialects.postgresql import insert
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession

from ...services.salesforce_gateway import SalesforceGateway
from .contact_enrichment_coordinator import EnrichmentMatch
from ...model_defs.enrichment_vault import EnrichmentVault
from ...models import EnrichmentUsage

logger = logging.getLogger(__name__)


@dataclass
class EnrichmentWriteResult:
    """Result of writing enrichments to Salesforce."""

    # Overall status
    success: bool
    total_records: int
    records_written: int
    records_failed: int

    # Detailed results
    successful_ids: List[str] = field(default_factory=list)
    failed_ids: List[str] = field(default_factory=list)
    errors: List[Dict[str, str]] = field(
        default_factory=list
    )  # [{"id": "...", "error": "..."}]

    # Statistics
    fields_updated: Dict[str, int] = field(
        default_factory=dict
    )  # {"email": 50, "phone": 30, ...}

    # Metadata
    dry_run: bool = False
    audit_trail: Optional[str] = None


class EnrichmentWriter:
    """
    Write enrichment results to Salesforce.

    Responsibilities:
    - Validate FLS permissions
    - Build update records
    - Use Bulk API for performance
    - Add audit trail metadata
    - Handle partial failures gracefully
    """

    # Custom field for audit trail (will be created if doesn't exist)
    AUDIT_FIELD_NAME = "FM_DataSource__c"

    def __init__(
        self,
        gateway: SalesforceGateway,
        scanner=None,
        validate_fls: bool = True,
        add_audit_trail: bool = True,
    ):
        """
        Initialize writer.

        Args:
            gateway: Salesforce gateway for writes
            scanner: MissingFieldScanner instance for field mapping (optional)
            validate_fls: Check FLS before writing
            add_audit_trail: Add FM_DataSource__c field to track enrichment source
        """
        self.gateway = gateway
        self.scanner = scanner
        self.validate_fls = validate_fls
        self.add_audit_trail = add_audit_trail

    async def write_enrichments(
        self,
        account_id: str,
        matches: List[EnrichmentMatch],
        dry_run: bool = False,
        db: Optional[AsyncSession] = None,
        usage_id: Optional[int] = None,
    ) -> EnrichmentWriteResult:
        """
        Write enrichment matches to Salesforce.

        Args:
            account_id: FoundryMatch account ID
            matches: List of enrichment matches to write
            dry_run: If True, validate but don't actually write
            db: Optional database session for archiving enriched data
            usage_id: Optional EnrichmentUsage ID for archiving

        Returns:
            EnrichmentWriteResult with detailed status
        """
        result = EnrichmentWriteResult(
            success=False,
            total_records=len(matches),
            records_written=0,
            records_failed=0,
            dry_run=dry_run,
        )

        if not matches:
            logger.info("No matches to write")
            result.success = True
            return result

        logger.info(
            f"Writing {len(matches)} enrichments to Salesforce (dry_run={dry_run})"
        )

        # Check if archival is enabled (only if db session provided)
        archive_enabled = False
        if db and usage_id and not dry_run:
            archive_enabled = await self._should_archive(db, account_id)
            if archive_enabled:
                logger.info(f"Enrichment archival enabled for account {account_id}")

        # Group by object type (Contact vs Lead)
        by_object = self._group_by_object(matches)

        # Process each object type
        for sfdc_object, object_matches in by_object.items():
            logger.info(f"Processing {len(object_matches)} {sfdc_object} records")

            # Validate FLS if enabled
            if self.validate_fls:
                fls_result = await self._validate_fls(
                    account_id, sfdc_object, object_matches
                )
                if not fls_result["valid"]:
                    # Remove fields without update permission
                    object_matches = self._filter_by_fls(
                        object_matches, fls_result["updateable_fields"]
                    )
                    if not object_matches:
                        logger.error(f"No updateable fields for {sfdc_object}")
                        continue

            # Build update records
            update_records = self._build_update_records(object_matches)

            if not update_records:
                logger.warning(f"No records to update for {sfdc_object}")
                continue

            # Fetch before values if archival is enabled
            before_lookup: Dict[str, Dict[str, Any]] = {}
            if archive_enabled:
                record_ids = [r["Id"] for r in update_records]
                field_names = list(
                    set(k for r in update_records for k in r.keys() if k != "Id")
                )
                before_lookup = await self._fetch_current_values(
                    sfdc_object, record_ids, field_names
                )
                logger.debug(f"Fetched before values for {len(before_lookup)} records")

            # Write to Salesforce (unless dry run)
            if dry_run:
                logger.info(
                    f"DRY RUN: Would update {len(update_records)} {sfdc_object} records"
                )
                result.records_written += len(update_records)
                result.successful_ids.extend([r["Id"] for r in update_records])
            else:
                write_result = await self._write_bulk(sfdc_object, update_records)
                result.records_written += write_result["success_count"]
                result.records_failed += write_result["failure_count"]
                result.successful_ids.extend(write_result["successful_ids"])
                result.failed_ids.extend(write_result["failed_ids"])
                result.errors.extend(write_result["errors"])

                # Archive successful writes if enabled
                if archive_enabled and write_result["success_count"] > 0:
                    try:
                        await self._archive_to_vault(
                            db,
                            usage_id=usage_id,
                            account_id=account_id,
                            sfdc_object=sfdc_object,
                            update_records=update_records,
                            write_result=write_result,
                            before_lookup=before_lookup,
                        )
                    except Exception as e:
                        logger.error(f"Archive failed but continuing: {e}")
                        # Don't fail the whole write if archival fails

            # Track field statistics
            for match in object_matches:
                for field_name in match.fields_to_update:
                    result.fields_updated[field_name] = (
                        result.fields_updated.get(field_name, 0) + 1
                    )

        # Set overall success
        result.success = result.records_failed == 0

        # Generate audit trail
        if self.add_audit_trail:
            result.audit_trail = self._generate_audit_trail()

        logger.info(
            f"Write complete: {result.records_written} succeeded, {result.records_failed} failed "
            f"(dry_run={dry_run})"
        )

        return result

    async def _validate_fls(
        self,
        account_id: str,
        sfdc_object: str,
        matches: List[EnrichmentMatch],
    ) -> Dict:
        """
        Validate field-level security for all fields we want to update.

        Returns:
            {"valid": bool, "updateable_fields": Set[str], "errors": List[str]}
        """
        # Collect all fields we want to update
        fields_to_check = set()
        for match in matches:
            for logical_field in match.fields_to_update:
                # Map logical field to SFDC field name
                sfdc_field = self._logical_to_sfdc_field(logical_field, sfdc_object)
                fields_to_check.add(sfdc_field)

        if self.add_audit_trail:
            fields_to_check.add(self.AUDIT_FIELD_NAME)

        logger.debug(f"Validating FLS for fields: {fields_to_check}")

        # Get field permissions via describe
        try:
            describe_result = await self.gateway.describe(sfdc_object)

            # Build field map: {field_name.lower(): field_metadata}
            field_map = {
                f["name"].lower(): f
                for f in describe_result.get("fields", [])
                if f.get("name")
            }

            # Check updateable flag for each field
            updateable_fields = set()
            errors = []

            for sfdc_field in fields_to_check:
                field_lower = sfdc_field.lower()
                field_meta = field_map.get(field_lower)

                if not field_meta:
                    errors.append(f"Field {sfdc_field} not found on {sfdc_object}")
                    logger.warning(f"Field {sfdc_field} not found on {sfdc_object}")
                    continue

                if not field_meta.get("updateable", False):
                    errors.append(
                        f"Field {sfdc_field} is not updateable (FLS permission denied)"
                    )
                    logger.warning(
                        f"Field {sfdc_field} on {sfdc_object} is not updateable"
                    )
                    continue

                # Field is updateable
                updateable_fields.add(sfdc_field)

            # Return validation result
            if not updateable_fields and fields_to_check:
                # All fields blocked by FLS
                return {
                    "valid": False,
                    "updateable_fields": updateable_fields,
                    "errors": errors,
                }

            return {
                "valid": True,
                "updateable_fields": updateable_fields,
                "errors": errors,  # Include warnings about non-updateable fields
            }

        except Exception as e:
            logger.error(f"FLS validation failed: {e}")
            return {
                "valid": False,
                "updateable_fields": set(),
                "errors": [f"Describe API failed: {str(e)}"],
            }

    def _filter_by_fls(
        self,
        matches: List[EnrichmentMatch],
        updateable_fields: Set[str],
    ) -> List[EnrichmentMatch]:
        """
        Filter matches to only include fields that are updateable.

        Returns new list of matches with filtered fields_to_update.
        """
        filtered = []

        for match in matches:
            # Filter fields_to_update
            allowed_fields = set()
            for logical_field in match.fields_to_update:
                sfdc_field = self._logical_to_sfdc_field(
                    logical_field, match.candidate.sfdc_object
                )
                if sfdc_field in updateable_fields:
                    allowed_fields.add(logical_field)

            if allowed_fields:
                # Create new match with filtered fields
                import copy

                new_match = copy.deepcopy(match)
                new_match.fields_to_update = allowed_fields
                filtered.append(new_match)

        return filtered

    def _build_update_records(self, matches: List[EnrichmentMatch]) -> List[Dict]:
        """
        Build Salesforce update records from matches.

        Returns:
            List of dicts ready for Bulk API: [{"Id": "...", "Email": "...", ...}]
        """
        records = []
        active_enrichable_fields: Set[str] = set()
        if self.scanner and hasattr(self.scanner, "get_enrichable_fields"):
            try:
                active_enrichable_fields = set(self.scanner.get_enrichable_fields())
            except Exception:  # pragma: no cover - defensive guard
                logger.debug(
                    "Failed to read enrichable fields from scanner", exc_info=True
                )

        for match in matches:
            record = {"Id": match.candidate.sfdc_id}

            # Add enriched fields
            sfdc_object = match.candidate.sfdc_object

            if "email" in match.fields_to_update and match.enriched_email:
                field_name = self._logical_to_sfdc_field("email", sfdc_object)
                record[field_name] = match.enriched_email

            if "phone" in match.fields_to_update and match.enriched_phone:
                field_name = self._logical_to_sfdc_field("phone", sfdc_object)
                record[field_name] = match.enriched_phone

            if "mobile_phone" in match.fields_to_update and match.enriched_mobile_phone:
                field_name = self._logical_to_sfdc_field("mobile_phone", sfdc_object)
                record[field_name] = match.enriched_mobile_phone

            if "title" in match.fields_to_update and match.enriched_title:
                field_name = self._logical_to_sfdc_field("title", sfdc_object)
                record[field_name] = match.enriched_title

            if (
                "linkedin_url" in match.fields_to_update
                and match.enriched_linkedin_url
                and "linkedin_url" in active_enrichable_fields
            ):
                field_name = self._logical_to_sfdc_field("linkedin_url", sfdc_object)
                if field_name.lower() == "linkedin_url":
                    logger.debug(
                        "Skipping linkedin_url update due to unresolved mapping (sfdc_object=%s)",
                        sfdc_object,
                    )
                else:
                    record[field_name] = match.enriched_linkedin_url

            # Add audit trail
            if self.add_audit_trail:
                record[self.AUDIT_FIELD_NAME] = self._generate_audit_trail()

            # Only include record if we're actually updating something
            if len(record) > 1:  # More than just Id
                records.append(record)

        return records

    async def _write_bulk(
        self,
        sfdc_object: str,
        records: List[Dict],
    ) -> Dict:
        """
        Write records via Salesforce Bulk API.

        Returns:
            {
                "success_count": int,
                "failure_count": int,
                "successful_ids": List[str],
                "failed_ids": List[str],
                "errors": List[Dict],
            }
        """
        logger.info(f"Writing {len(records)} {sfdc_object} records via Bulk API")

        try:
            # Use gateway's bulk update method
            result = await self.gateway.bulk_update(sfdc_object, records)

            if result.get("error"):
                logger.error(f"Bulk update failed: {result['error']}")
                return {
                    "success_count": 0,
                    "failure_count": len(records),
                    "successful_ids": [],
                    "failed_ids": [r["Id"] for r in records],
                    "errors": [
                        {"id": r["Id"], "error": result["error"]} for r in records
                    ],
                }

            # Process results
            successes = result.get("successfulResults", [])
            failures = result.get("failedResults", [])

            return {
                "success_count": len(successes),
                "failure_count": len(failures),
                "successful_ids": [s.get("id") for s in successes if s.get("id")],
                "failed_ids": [f.get("id") for f in failures if f.get("id")],
                "errors": [
                    {
                        "id": f.get("id"),
                        "error": f.get("errors", [{}])[0].get(
                            "message", "Unknown error"
                        ),
                    }
                    for f in failures
                ],
            }

        except Exception as e:
            logger.error(f"Bulk write exception: {e}")
            return {
                "success_count": 0,
                "failure_count": len(records),
                "successful_ids": [],
                "failed_ids": [r["Id"] for r in records],
                "errors": [{"id": r["Id"], "error": str(e)} for r in records],
            }

    @staticmethod
    def _group_by_object(
        matches: List[EnrichmentMatch],
    ) -> Dict[str, List[EnrichmentMatch]]:
        """Group matches by Salesforce object type (Contact vs Lead)."""
        by_object = {}

        for match in matches:
            sfdc_object = match.candidate.sfdc_object
            if sfdc_object not in by_object:
                by_object[sfdc_object] = []
            by_object[sfdc_object].append(match)

        return by_object

    def _logical_to_sfdc_field(self, logical_field: str, sfdc_object: str) -> str:
        """
        Map logical field name to actual Salesforce field name.

        Uses scanner's field mapping if available (supports custom fields),
        otherwise falls back to standard mapping.
        """
        # Delegate to scanner if available
        if self.scanner:
            return self.scanner._get_field_name(sfdc_object, logical_field)

        # Fallback to standard mapping
        mapping = {
            "email": "Email",
            "phone": "Phone",
            "mobile_phone": "MobilePhone",
            "title": "Title",
        }
        return mapping.get(logical_field, logical_field)

    @staticmethod
    def _generate_audit_trail() -> str:
        """Generate audit trail string for FM_DataSource__c field."""
        today = datetime.now().strftime("%Y-%m-%d")
        return f"PDL_{today}"

    # -------------------------------------------------------------------------
    # Archival methods (for audit trail and data recovery)
    # -------------------------------------------------------------------------

    @staticmethod
    async def _should_archive(db: AsyncSession, account_id: str) -> bool:
        """Check if account has archival enabled."""
        try:
            row = await db.execute(
                sa.text(
                    "SELECT enrichment_archival_enabled FROM accounts WHERE id = :aid"
                ),
                {"aid": account_id},
            )
            val = row.scalar_one_or_none()
            return bool(val) if val is not None else True
        except Exception as e:
            logger.warning(
                f"Failed to check archival setting for account {account_id}: {e}"
            )
            return True  # Default to enabled if check fails

    async def _fetch_current_values(
        self,
        sfdc_object: str,
        record_ids: List[str],
        field_names: List[str],
    ) -> Dict[str, Dict[str, Any]]:
        """Fetch current field values from SFDC before overwriting (for before/after audit)."""
        if not record_ids or not field_names:
            return {}

        try:
            # Build SOQL: SELECT Id, Email, Title, MobilePhone FROM Lead WHERE Id IN (...)
            fields_str = ", ".join(["Id"] + sorted(set(field_names)))
            ids_str = "','".join(record_ids)
            soql = f"SELECT {fields_str} FROM {sfdc_object} WHERE Id IN ('{ids_str}')"

            records = await self.gateway.query(soql)
            result = {}
            for rec in records.get("records", []):
                rec_id = rec.get("Id")
                if rec_id:
                    result[rec_id] = {k: v for k, v in rec.items() if k != "Id"}
            return result
        except Exception as e:
            logger.warning(f"Failed to fetch current values for archival: {e}")
            return {}

    @staticmethod
    async def _archive_to_vault(
        db: AsyncSession,
        *,
        usage_id: int,
        account_id: str,
        sfdc_object: str,
        update_records: List[Dict],
        write_result: Dict,
        before_lookup: Dict[str, Dict[str, Any]],
    ) -> None:
        """
        Field-level archive. Writes one row per (record, field).

        Args:
            db: Database session
            usage_id: EnrichmentUsage.id (ledger reference)
            account_id: Account ID
            sfdc_object: SFDC object type (Lead, Contact, Account)
            update_records: Records that were sent to SFDC [{"Id":"003...", "Email":"x", ...}]
            write_result: Result from _write_bulk with success_ids
            before_lookup: Dict of {record_id: {field: value}} for before state
        """
        success_ids = set(write_result.get("successful_ids", []))

        rows = []
        for rec in update_records:
            rid = rec.get("Id") or rec.get("id")
            if not rid:
                continue
            if success_ids and rid not in success_ids:
                continue  # Only archive successful writes

            before_map = before_lookup.get(rid, {})
            for k, v in rec.items():
                if k in ("Id", "id"):
                    continue
                vb = before_map.get(k)  # May be None if no snapshot was taken
                rows.append(
                    {
                        "usage_id": usage_id,
                        "account_id": account_id,
                        "sfdc_object": sfdc_object,
                        "sfdc_record_id": rid,
                        "field_name": k,
                        "value_before": None if vb is None else str(vb),
                        "value_after": "" if v is None else str(v),
                        "provider_confidence": None,  # Can be filled if you carry scores forward
                    }
                )

        if not rows:
            return

        # Use INSERT ... ON CONFLICT DO NOTHING for idempotency
        stmt = insert(EnrichmentVault).values(rows)
        stmt = stmt.on_conflict_do_nothing(
            index_elements=["usage_id", "sfdc_record_id", "field_name"]
        )

        try:
            await db.execute(stmt)
            await db.execute(
                sa.update(EnrichmentUsage)
                .where(EnrichmentUsage.id == usage_id)
                .values(has_archived_data=True)
            )
            logger.info(
                f"Archived {len(rows)} field-level records to vault for usage_id={usage_id}"
            )
        except IntegrityError as e:
            # Safe to ignore duplicate rows caused by retries
            logger.debug(f"Vault archive conflict (expected on retry): {e}")
        except Exception as e:
            logger.error(f"Failed to archive to vault: {e}")
