from __future__ import annotations

from dataclasses import dataclass
from typing import Any

PRECEDENCE = {
    "trust_boundary": 3,
    "authorization_policy": 3,
    "data_classification": 2,
    "input_validation": 2,
    "security_config": 2,
    "secrets_management": 2,
    "pipeline_policy": 2,
    "error_handling": 2,
    "approved_pattern": 1,
    "anti_pattern": 1,
}


@dataclass(frozen=True)
class RequirementCandidate:
    id: str
    text: str
    severity: str
    source: str
    source_type: str


def assess(
    context: dict[str, Any],
    services: list[str] | None = None,
    data_types: list[str] | None = None,
    boundary_crossing: str | None = None,
    auth_scope: str | None = None,
    input_sources: list[str] | None = None,
    action: str | None = None,
    description: str | None = None,
) -> dict[str, Any]:
    del description  # Reserved for future disambiguation.

    service_values = _normalize_values(services or [])
    data_values = _normalize_values(data_types or [])
    input_source_values = _normalize_values(input_sources or [])
    boundary_value = boundary_crossing.strip() if isinstance(boundary_crossing, str) else None
    auth_scope_value = auth_scope.strip() if isinstance(auth_scope, str) else None
    action_value = action.strip() if isinstance(action, str) else None

    warnings: list[str] = []

    trust_boundaries = _index_by_id(context.get("trust_boundaries", []))
    data_classifications = _index_by_id(context.get("data_classifications", []))
    services_index = _index_by_id(context.get("services", []))
    authorization_policies = _index_by_id(context.get("authorization_policies", []))
    input_validations = _index_by_id(context.get("input_validations", []))

    matched_boundaries: list[str] = []
    matched_data_types: list[str] = []
    matched_services: list[str] = []
    matched_auth_scopes: list[str] = []
    matched_input_sources: list[str] = []

    boundary_missing = False
    if boundary_value:
        boundary = trust_boundaries.get(boundary_value)
        if boundary is None:
            warnings.append(
                f'Unknown boundary_crossing "{boundary_value}". Expected one of: '
                f"{', '.join(sorted(trust_boundaries.keys())) or '(none defined)'}"
            )
            boundary_missing = True
        else:
            matched_boundaries.append(boundary_value)

    if auth_scope_value:
        policy = authorization_policies.get(auth_scope_value)
        if policy is None:
            warnings.append(
                f'Unknown auth_scope "{auth_scope_value}". Expected one of: '
                f"{', '.join(sorted(authorization_policies.keys())) or '(none defined)'}"
            )
        else:
            matched_auth_scopes.append(auth_scope_value)

    for data_type in data_values:
        if data_type in data_classifications:
            matched_data_types.append(data_type)
        else:
            warnings.append(f'Unknown data type "{data_type}".')

    if service_values:
        if services_index:
            for service_id in service_values:
                if service_id in services_index:
                    matched_services.append(service_id)
                else:
                    warnings.append(f'Unknown service "{service_id}".')
        else:
            warnings.append(
                "Services were provided but schema does not define services[]; service matching skipped."
            )

    for input_source in input_source_values:
        if input_source in input_validations:
            matched_input_sources.append(input_source)
        else:
            warnings.append(f'Unknown input source "{input_source}".')

    candidates: list[RequirementCandidate] = []

    for boundary_id in matched_boundaries:
        boundary = trust_boundaries[boundary_id]
        for requirement in boundary.get("crossing_requirements", []):
            candidate = _as_requirement_candidate(
                requirement=requirement,
                source=f"trust_boundary:{boundary_id}",
                source_type="trust_boundary",
            )
            if candidate is not None:
                candidates.append(candidate)

    for data_id in matched_data_types:
        classification = data_classifications[data_id]
        for requirement in classification.get("handling_rules", []):
            candidate = _as_requirement_candidate(
                requirement=requirement,
                source=f"data_classification:{data_id}",
                source_type="data_classification",
            )
            if candidate is not None:
                candidates.append(candidate)

    for policy_id in matched_auth_scopes:
        policy = authorization_policies[policy_id]
        for requirement in policy.get("required_controls", []):
            candidate = _as_requirement_candidate(
                requirement=requirement,
                source=f"authorization_policy:{policy_id}",
                source_type="authorization_policy",
            )
            if candidate is not None:
                candidates.append(candidate)

    for input_source_id in matched_input_sources:
        validation = input_validations[input_source_id]
        for requirement in validation.get("validation_rules", []):
            candidate = _as_requirement_candidate(
                requirement=requirement,
                source=f"input_validation:{input_source_id}",
                source_type="input_validation",
            )
            if candidate is not None:
                candidates.append(candidate)

    relevant_approved_patterns = [
        pattern
        for pattern in context.get("approved_patterns", [])
        if _is_pattern_relevant(
            pattern=pattern,
            matched_boundaries=matched_boundaries,
            matched_data_types=matched_data_types,
            matched_services=matched_services,
            matched_auth_scopes=matched_auth_scopes,
            matched_input_sources=matched_input_sources,
            action=action_value,
        )
    ]

    relevant_anti_patterns = [
        pattern
        for pattern in context.get("anti_patterns", [])
        if _is_pattern_relevant(
            pattern=pattern,
            matched_boundaries=matched_boundaries,
            matched_data_types=matched_data_types,
            matched_services=matched_services,
            matched_auth_scopes=matched_auth_scopes,
            matched_input_sources=matched_input_sources,
            action=action_value,
        )
    ]

    relevant_security_configs = [
        policy
        for policy in context.get("security_configs", [])
        if _is_pattern_relevant(
            pattern=policy,
            matched_boundaries=matched_boundaries,
            matched_data_types=matched_data_types,
            matched_services=matched_services,
            matched_auth_scopes=matched_auth_scopes,
            matched_input_sources=matched_input_sources,
            action=action_value,
        )
    ]

    relevant_secret_management = [
        policy
        for policy in context.get("secrets_management", [])
        if _is_pattern_relevant(
            pattern=policy,
            matched_boundaries=matched_boundaries,
            matched_data_types=matched_data_types,
            matched_services=matched_services,
            matched_auth_scopes=matched_auth_scopes,
            matched_input_sources=matched_input_sources,
            action=action_value,
        )
    ]

    relevant_pipeline_policies = [
        policy
        for policy in context.get("pipeline_policies", [])
        if _is_pattern_relevant(
            pattern=policy,
            matched_boundaries=matched_boundaries,
            matched_data_types=matched_data_types,
            matched_services=matched_services,
            matched_auth_scopes=matched_auth_scopes,
            matched_input_sources=matched_input_sources,
            action=action_value,
        )
    ]

    relevant_error_handling = [
        policy
        for policy in context.get("error_handling", [])
        if _is_pattern_relevant(
            pattern=policy,
            matched_boundaries=matched_boundaries,
            matched_data_types=matched_data_types,
            matched_services=matched_services,
            matched_auth_scopes=matched_auth_scopes,
            matched_input_sources=matched_input_sources,
            action=action_value,
        )
    ]

    for pattern in relevant_approved_patterns:
        pattern_id = str(pattern.get("id", "unknown"))
        for requirement in pattern.get("requirements", []):
            candidate = _as_requirement_candidate(
                requirement=requirement,
                source=f"approved_pattern:{pattern_id}",
                source_type="approved_pattern",
            )
            if candidate is not None:
                candidates.append(candidate)

    for pattern in relevant_anti_patterns:
        pattern_id = str(pattern.get("id", "unknown"))
        for requirement in pattern.get("requirements", []):
            candidate = _as_requirement_candidate(
                requirement=requirement,
                source=f"anti_pattern:{pattern_id}",
                source_type="anti_pattern",
            )
            if candidate is not None:
                candidates.append(candidate)

    for policy in relevant_security_configs:
        policy_id = str(policy.get("id", "unknown"))
        for requirement in policy.get("rules", []):
            candidate = _as_requirement_candidate(
                requirement=requirement,
                source=f"security_config:{policy_id}",
                source_type="security_config",
            )
            if candidate is not None:
                candidates.append(candidate)

    for policy in relevant_secret_management:
        policy_id = str(policy.get("id", "unknown"))
        for requirement in policy.get("rules", []):
            candidate = _as_requirement_candidate(
                requirement=requirement,
                source=f"secrets_management:{policy_id}",
                source_type="secrets_management",
            )
            if candidate is not None:
                candidates.append(candidate)

    for policy in relevant_pipeline_policies:
        policy_id = str(policy.get("id", "unknown"))
        for requirement in policy.get("rules", []):
            candidate = _as_requirement_candidate(
                requirement=requirement,
                source=f"pipeline_policy:{policy_id}",
                source_type="pipeline_policy",
            )
            if candidate is not None:
                candidates.append(candidate)

    for policy in relevant_error_handling:
        policy_id = str(policy.get("id", "unknown"))
        for requirement in policy.get("rules", []):
            candidate = _as_requirement_candidate(
                requirement=requirement,
                source=f"error_handling:{policy_id}",
                source_type="error_handling",
            )
            if candidate is not None:
                candidates.append(candidate)

    merged_requirements, conflicts = _merge_requirements(candidates)

    matched_any = bool(
        matched_boundaries
        or matched_data_types
        or matched_services
        or matched_auth_scopes
        or matched_input_sources
    )
    no_match = boundary_missing or not matched_any
    if no_match and not warnings:
        warnings.append("No parameters matched canonical ids in the security context.")

    return {
        "requirements": merged_requirements,
        "approved_patterns": [_serialize_pattern(item) for item in relevant_approved_patterns],
        "anti_patterns": [_serialize_anti_pattern(item) for item in relevant_anti_patterns],
        "security_configs": [_serialize_policy(item) for item in relevant_security_configs],
        "secrets_management": [_serialize_policy(item) for item in relevant_secret_management],
        "pipeline_policies": [_serialize_policy(item) for item in relevant_pipeline_policies],
        "error_handling": [_serialize_policy(item) for item in relevant_error_handling],
        "matched": {
            "trust_boundaries": matched_boundaries,
            "data_types": matched_data_types,
            "services": matched_services,
            "authorization_policies": matched_auth_scopes,
            "input_validations": matched_input_sources,
        },
        "warnings": warnings,
        "conflicts": conflicts,
        "no_match": no_match,
    }


def build_summary(context: dict[str, Any]) -> dict[str, Any]:
    trust_boundaries = context.get("trust_boundaries", [])
    data_classifications = context.get("data_classifications", [])
    approved_patterns = context.get("approved_patterns", [])
    anti_patterns = context.get("anti_patterns", [])

    return {
        "version": context.get("version"),
        "counts": {
            "services": len(context.get("services", [])),
            "trust_boundaries": len(trust_boundaries),
            "data_classifications": len(data_classifications),
            "approved_patterns": len(approved_patterns),
            "anti_patterns": len(anti_patterns),
            "authorization_policies": len(context.get("authorization_policies", [])),
            "input_validations": len(context.get("input_validations", [])),
            "security_configs": len(context.get("security_configs", [])),
            "secrets_management": len(context.get("secrets_management", [])),
            "pipeline_policies": len(context.get("pipeline_policies", [])),
            "error_handling": len(context.get("error_handling", [])),
        },
        "trust_boundary_ids": [item.get("id") for item in trust_boundaries if isinstance(item, dict)],
        "data_classification_ids": [
            item.get("id") for item in data_classifications if isinstance(item, dict)
        ],
        "authorization_policy_ids": [
            item.get("id")
            for item in context.get("authorization_policies", [])
            if isinstance(item, dict)
        ],
        "input_validation_ids": [
            item.get("id")
            for item in context.get("input_validations", [])
            if isinstance(item, dict)
        ],
        "security_config_ids": [
            item.get("id")
            for item in context.get("security_configs", [])
            if isinstance(item, dict)
        ],
        "secrets_management_ids": [
            item.get("id")
            for item in context.get("secrets_management", [])
            if isinstance(item, dict)
        ],
        "pipeline_policy_ids": [
            item.get("id")
            for item in context.get("pipeline_policies", [])
            if isinstance(item, dict)
        ],
        "error_handling_ids": [
            item.get("id")
            for item in context.get("error_handling", [])
            if isinstance(item, dict)
        ],
        "notes": [
            "Call aegis_assess whenever work crosses trust boundaries, touches classified data,",
            "changes auth behavior, handles untrusted input, adjusts security config,",
            "modifies CI/CD or secrets flows, or integrates external services.",
        ],
    }


def _index_by_id(items: Any) -> dict[str, dict[str, Any]]:
    indexed: dict[str, dict[str, Any]] = {}
    if not isinstance(items, list):
        return indexed

    for item in items:
        if not isinstance(item, dict):
            continue
        item_id = item.get("id")
        if isinstance(item_id, str):
            indexed[item_id] = item
    return indexed


def _normalize_values(values: list[str]) -> list[str]:
    normalized: list[str] = []
    seen: set[str] = set()
    for raw in values:
        if not isinstance(raw, str):
            continue
        value = raw.strip()
        if not value or value in seen:
            continue
        normalized.append(value)
        seen.add(value)
    return normalized


def _is_pattern_relevant(
    pattern: dict[str, Any],
    matched_boundaries: list[str],
    matched_data_types: list[str],
    matched_services: list[str],
    matched_auth_scopes: list[str],
    matched_input_sources: list[str],
    action: str | None,
) -> bool:
    when = pattern.get("when")
    if not isinstance(when, dict) or not when:
        return True

    if _has_required_intersection(when.get("boundary_crossings"), matched_boundaries) is False:
        return False

    if _has_required_intersection(when.get("data_types"), matched_data_types) is False:
        return False

    if _has_required_intersection(when.get("services"), matched_services) is False:
        return False

    if _has_required_intersection(when.get("auth_scopes"), matched_auth_scopes) is False:
        return False

    if _has_required_intersection(when.get("input_sources"), matched_input_sources) is False:
        return False

    required_actions = when.get("actions")
    if isinstance(required_actions, list) and required_actions:
        if not isinstance(action, str) or action not in required_actions:
            return False

    return True


def _has_required_intersection(required: Any, actual: list[str]) -> bool | None:
    if not isinstance(required, list) or not required:
        return None
    return bool(set(required).intersection(actual))


def _as_requirement_candidate(
    requirement: Any,
    source: str,
    source_type: str,
) -> RequirementCandidate | None:
    if not isinstance(requirement, dict):
        return None

    requirement_id = requirement.get("id")
    text = requirement.get("text")
    severity = requirement.get("severity")
    if not isinstance(requirement_id, str) or not isinstance(text, str) or not isinstance(severity, str):
        return None

    return RequirementCandidate(
        id=requirement_id,
        text=text,
        severity=severity,
        source=source,
        source_type=source_type,
    )


def _merge_requirements(
    candidates: list[RequirementCandidate],
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
    merged: dict[str, RequirementCandidate] = {}
    conflicts: list[dict[str, Any]] = []

    for candidate in candidates:
        existing = merged.get(candidate.id)
        if existing is None:
            merged[candidate.id] = candidate
            continue

        if existing.text == candidate.text and existing.severity == candidate.severity:
            if _rank(candidate) > _rank(existing):
                merged[candidate.id] = candidate
            continue

        winner, loser = _choose_winner(existing, candidate)
        merged[candidate.id] = winner
        conflicts.append(
            {
                "requirement_id": candidate.id,
                "chosen_source": winner.source,
                "discarded_source": loser.source,
                "reason": (
                    "Conflicting requirement text/severity; applied deterministic precedence "
                    "trust/authorization sources > data/config/input/policy sources > pattern defaults."
                ),
            }
        )

    ordered = sorted(
        merged.values(),
        key=lambda item: (-_rank(item), item.source, item.id),
    )

    requirements = [
        {
            "id": item.id,
            "source": item.source,
            "requirement": item.text,
            "severity": item.severity,
        }
        for item in ordered
    ]

    return requirements, conflicts


def _rank(candidate: RequirementCandidate) -> int:
    return PRECEDENCE.get(candidate.source_type, 0)


def _choose_winner(
    first: RequirementCandidate,
    second: RequirementCandidate,
) -> tuple[RequirementCandidate, RequirementCandidate]:
    first_key = (_rank(first), first.source)
    second_key = (_rank(second), second.source)
    if second_key > first_key:
        return second, first
    return first, second


def _serialize_pattern(pattern: Any) -> dict[str, Any]:
    if not isinstance(pattern, dict):
        return {}

    response = {
        "id": pattern.get("id"),
        "name": pattern.get("name"),
        "description": pattern.get("description"),
    }
    if "example" in pattern:
        response["example"] = pattern.get("example")
    return response


def _serialize_anti_pattern(pattern: Any) -> dict[str, Any]:
    if not isinstance(pattern, dict):
        return {}

    return {
        "id": pattern.get("id"),
        "name": pattern.get("name"),
        "description": pattern.get("description"),
        "severity": pattern.get("severity"),
    }


def _serialize_policy(policy: Any) -> dict[str, Any]:
    if not isinstance(policy, dict):
        return {}

    response = {
        "id": policy.get("id"),
        "name": policy.get("name"),
        "description": policy.get("description"),
    }
    if "scope" in policy:
        response["scope"] = policy.get("scope")
    return response
