"""
Dimension Resolver Service

Applies accounting code dimension requirements when configured. Dimensions are
optional by default: postings succeed without assignments unless
settings.enforce_required_accounting_dimensions is True and the account marks a
dimension as required (AccountingCodeDimensionRequirement.is_required).
"""

from typing import Any, Dict, List, Optional, Tuple, Union

from sqlalchemy.orm import Session
from sqlalchemy import and_

from app.models.accounting_code_dimensions import AccountingCodeDimensionRequirement
from app.models.accounting_dimensions import AccountingDimensionValue, AccountingDimensionAssignment
from app.models.accounting import AccountingCode
from app.services.ifrs_accounting_service import IFRSAccountingService
from app.utils.logger import get_logger

logger = get_logger(__name__)

# Document FK fields store AccountingDimensionValue.id values (not dimension headers).
_DIMENSION_VALUE_ATTRS = (
    "cost_center_id",
    "project_id",
    "department_id",
    "production_cost_center_id",
    "sales_cost_center_id",
    "from_cost_center_id",
    "to_cost_center_id",
    "from_project_id",
    "to_project_id",
)


def collect_dimension_value_ids(*sources: Any) -> List[str]:
    """
    Gather dimension value IDs from documents, dicts, or explicit ID lists.

    Transaction models store AccountingDimensionValue.id on cost_center_id /
    project_id / department_id fields.
    """
    ids: List[str] = []
    seen: set = set()

    def add(raw: Any) -> None:
        if raw is None or raw == "":
            return
        sid = str(raw)
        if sid not in seen:
            seen.add(sid)
            ids.append(sid)

    for source in sources:
        if source is None:
            continue
        if isinstance(source, (list, tuple, set)):
            for item in source:
                if isinstance(item, str) or item is None:
                    add(item)
                else:
                    add(getattr(item, "id", item))
            continue
        if isinstance(source, dict):
            for key in _DIMENSION_VALUE_ATTRS:
                add(source.get(key))
            continue
        for attr in _DIMENSION_VALUE_ATTRS:
            add(getattr(source, attr, None))

    return ids


def resolve_dimension_strict_mode(explicit_strict: Optional[bool] = None) -> bool:
    """True only when caller requests strict or ENFORCE_REQUIRED_ACCOUNTING_DIMENSIONS is set."""
    if explicit_strict is not None:
        return bool(explicit_strict)
    try:
        from app.core.config import settings

        return bool(getattr(settings, "enforce_required_accounting_dimensions", False))
    except Exception:
        return False


class DimensionResolverService:
    """Resolves and applies default dimensions for accounting codes."""

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

    def build_provided_map_from_values(
        self, dimension_value_ids: Optional[List[str]]
    ) -> Dict[str, str]:
        """Convert dimension value IDs into dimension_id -> dimension_value_id."""
        cleaned = collect_dimension_value_ids(dimension_value_ids)
        if not cleaned:
            return {}

        values = (
            self.db.query(AccountingDimensionValue)
            .filter(AccountingDimensionValue.id.in_(cleaned))
            .all()
        )
        return {v.dimension_id: v.id for v in values if v.dimension_id}

    def resolve_for_account(
        self,
        accounting_code_id: str,
        provided_dimensions: Optional[Dict[str, str]] = None,
    ) -> Tuple[List[Dict[str, str]], List[str]]:
        """
        Resolve dimension assignments for an account.

        Returns a tuple: (resolved_assignments, missing_required_dimension_ids)
        Each resolved assignment dict contains: dimension_id, dimension_value_id, assignment_method.
        """
        provided = provided_dimensions or {}
        resolved: List[Dict[str, str]] = []
        missing_required: List[str] = []

        requirements = (
            self.db.query(AccountingCodeDimensionRequirement)
            .filter(AccountingCodeDimensionRequirement.accounting_code_id == accounting_code_id)
            .order_by(AccountingCodeDimensionRequirement.priority)
            .all()
        )

        if not requirements:
            return resolved, missing_required

        for req in requirements:
            dim_id = req.dimension_id
            chosen_value_id = provided.get(dim_id) or req.default_dimension_value_id

            if not chosen_value_id:
                if req.is_required:
                    missing_required.append(dim_id)
                continue

            dim_value = (
                self.db.query(AccountingDimensionValue)
                .filter(
                    and_(
                        AccountingDimensionValue.id == chosen_value_id,
                        AccountingDimensionValue.dimension_id == dim_id,
                    )
                )
                .first()
            )

            if not dim_value:
                if req.is_required:
                    missing_required.append(dim_id)
                continue

            resolved.append(
                {
                    "dimension_id": dim_id,
                    "dimension_value_id": chosen_value_id,
                    "assignment_method": "provided" if dim_id in provided else "automatic",
                }
            )

        return resolved, missing_required

    def apply_to_entry(
        self,
        journal_entry_id: str,
        accounting_code_id: str,
        provided_dimensions: Optional[Dict[str, str]] = None,
        *,
        strict: Optional[bool] = None,
    ) -> None:
        """
        Apply resolved dimensions to a journal entry when values exist.

        Does not block posting when dimensions are omitted unless strict enforcement
        is enabled (global setting or strict=True).
        """
        resolved, missing_required = self.resolve_for_account(
            accounting_code_id=accounting_code_id,
            provided_dimensions=provided_dimensions,
        )

        enforce = resolve_dimension_strict_mode(strict)

        if missing_required:
            if enforce:
                raise ValueError(
                    "Missing required dimensions for account "
                    f"{accounting_code_id}: {', '.join(missing_required)}"
                )
            logger.debug(
                "Optional dimension(s) not set for account %s: %s",
                accounting_code_id,
                ", ".join(missing_required),
            )

        if not resolved:
            return

        account = self.db.query(AccountingCode).get(accounting_code_id)
        if not account:
            raise ValueError(f"Accounting code not found: {accounting_code_id}")

        reporting_tag = account.reporting_tag
        ifrs_category = None
        if reporting_tag:
            allowed_ifrs_tags = set(IFRSAccountingService.IFRS_REPORTING_CATEGORIES.keys())
            if reporting_tag in allowed_ifrs_tags:
                ifrs_category = IFRSAccountingService.IFRS_REPORTING_CATEGORIES.get(
                    reporting_tag
                )
            elif enforce:
                raise ValueError(
                    f"Invalid IFRS tag '{reporting_tag}' on account {account.code}"
                )

        for item in resolved:
            exists = (
                self.db.query(AccountingDimensionAssignment)
                .filter(
                    and_(
                        AccountingDimensionAssignment.journal_entry_id == journal_entry_id,
                        AccountingDimensionAssignment.dimension_id == item["dimension_id"],
                    )
                )
                .first()
            )
            if exists:
                continue

            assignment = AccountingDimensionAssignment(
                journal_entry_id=journal_entry_id,
                dimension_id=item["dimension_id"],
                dimension_value_id=item["dimension_value_id"],
                allocation_percentage=100.0,
                allocation_amount=None,
                assignment_method=item["assignment_method"],
                notes=None,
                ifrs_reporting_tag=reporting_tag,
                ifrs_category=ifrs_category,
            )
            self.db.add(assignment)

        logger.debug(
            "Applied %s dimension assignments to journal_entry_id=%s",
            len(resolved),
            journal_entry_id,
        )

    def apply_to_entries(
        self,
        journal_entries: List,
        provided_dimension_value_ids: Optional[Union[List[str], Any]] = None,
        *,
        document: Any = None,
        strict: Optional[bool] = None,
    ) -> None:
        """
        Apply dimension defaults/overrides to journal lines (optional unless enforced).

        Pass document=Purchase/Sale/etc. and/or explicit value IDs.
        """
        if not journal_entries:
            return

        value_ids = collect_dimension_value_ids(document, provided_dimension_value_ids)
        provided_map = self.build_provided_map_from_values(value_ids)
        enforce = resolve_dimension_strict_mode(strict)

        for entry in journal_entries:
            if not getattr(entry, "id", None):
                self.db.flush()
            try:
                self.apply_to_entry(
                    journal_entry_id=entry.id,
                    accounting_code_id=entry.accounting_code_id,
                    provided_dimensions=provided_map,
                    strict=enforce,
                )
            except ValueError as exc:
                if enforce:
                    raise
                logger.warning(
                    "Skipped dimension assignment for journal_entry %s: %s",
                    getattr(entry, "id", None),
                    exc,
                )

    def apply_from_document(
        self,
        journal_entries: List,
        document: Any,
        *,
        strict: Optional[bool] = None,
    ) -> None:
        """Apply dimensions from a source document when provided (always optional by default)."""
        self.apply_to_entries(journal_entries, document=document, strict=strict)
