"""
ID Wispera Delegation Chain Management
Track credential provenance through agent delegation
"""

from datetime import datetime, timedelta
from typing import Optional

from id_wispera.types import (
    Passport,
    DelegationHop,
    AuditAction,
)
from id_wispera.vault import Vault
from id_wispera.audit import log_action


# ============================================================================
# Delegation Chain Operations
# ============================================================================

def add_delegation(
    vault: Vault,
    passport_id: str,
    delegation_hop: DelegationHop,
    actor: str,
) -> Passport:
    """
    Add a delegation hop to a passport.
    The granted_at field is automatically set to the current time.

    Args:
        vault: The unlocked vault
        passport_id: The passport to delegate
        delegation_hop: Delegation details (from_entity, to, scope, expires_at, notes)
        actor: Who is performing the delegation

    Returns:
        The updated Passport

    Raises:
        ValueError: If passport not found
    """
    passport = vault.get_passport(passport_id)
    if passport is None:
        raise ValueError(f"Passport not found: {passport_id}")

    # Create hop with auto-set granted_at
    hop = DelegationHop(
        from_entity=delegation_hop.from_entity,
        to=delegation_hop.to,
        granted_at=datetime.utcnow(),
        scope=delegation_hop.scope,
        expires_at=delegation_hop.expires_at,
        notes=delegation_hop.notes,
    )

    updated_chain = list(passport.delegation_chain or [])
    updated_chain.append(hop)
    passport.delegation_chain = updated_chain
    passport.updated_at = datetime.utcnow()

    vault.update_passport(passport)

    log_action(
        vault,
        passport_id=passport_id,
        action=AuditAction.MODIFIED,
        actor=actor,
        details=f'Delegated credential from "{delegation_hop.from_entity}" to "{delegation_hop.to}"',
        metadata={
            "delegationFrom": delegation_hop.from_entity,
            "delegationTo": delegation_hop.to,
            "delegationScope": delegation_hop.scope,
        },
    )

    return passport


def revoke_delegation(
    vault: Vault,
    passport_id: str,
    actor: str,
) -> Passport:
    """
    Remove the last delegation hop from a passport.

    Args:
        vault: The unlocked vault
        passport_id: The passport ID
        actor: Who is revoking the delegation

    Returns:
        The updated Passport

    Raises:
        ValueError: If passport not found or no delegation to revoke
    """
    passport = vault.get_passport(passport_id)
    if passport is None:
        raise ValueError(f"Passport not found: {passport_id}")

    if not passport.delegation_chain or len(passport.delegation_chain) == 0:
        raise ValueError("No delegation to revoke")

    revoked_hop = passport.delegation_chain[-1]
    updated_chain = passport.delegation_chain[:-1]

    passport.delegation_chain = updated_chain
    passport.updated_at = datetime.utcnow()

    vault.update_passport(passport)

    log_action(
        vault,
        passport_id=passport_id,
        action=AuditAction.MODIFIED,
        actor=actor,
        details=f'Revoked delegation from "{revoked_hop.from_entity}" to "{revoked_hop.to}"',
        metadata={"revokedDelegation": revoked_hop.model_dump(by_alias=True, mode="json")},
    )

    return passport


def clear_delegations(
    vault: Vault,
    passport_id: str,
    actor: str,
) -> Passport:
    """
    Clear all delegations from a passport.

    Args:
        vault: The unlocked vault
        passport_id: The passport ID
        actor: Who is clearing the delegations

    Returns:
        The updated Passport

    Raises:
        ValueError: If passport not found
    """
    passport = vault.get_passport(passport_id)
    if passport is None:
        raise ValueError(f"Passport not found: {passport_id}")

    previous_count = len(passport.delegation_chain) if passport.delegation_chain else 0

    passport.delegation_chain = []
    passport.updated_at = datetime.utcnow()

    vault.update_passport(passport)

    log_action(
        vault,
        passport_id=passport_id,
        action=AuditAction.MODIFIED,
        actor=actor,
        details=f"Cleared all delegations ({previous_count} hops removed)",
        metadata={"clearedDelegationCount": previous_count},
    )

    return passport


# ============================================================================
# Delegation Chain Analysis
# ============================================================================

def get_delegation_depth(passport: Passport) -> int:
    """
    Get the delegation depth of a passport.

    Args:
        passport: The passport to check

    Returns:
        Number of delegation hops
    """
    return len(passport.delegation_chain) if passport.delegation_chain else 0


def get_current_holder(passport: Passport) -> str:
    """
    Get the current holder (last in delegation chain, or human owner if no chain).

    Args:
        passport: The passport to check

    Returns:
        Entity name of the current holder
    """
    if not passport.delegation_chain or len(passport.delegation_chain) == 0:
        return passport.human_owner
    return passport.delegation_chain[-1].to


def get_original_issuer(passport: Passport) -> str:
    """
    Get the original issuer (human owner).

    Args:
        passport: The passport to check

    Returns:
        Human owner name
    """
    return passport.human_owner


def get_all_delegation_parties(passport: Passport) -> list[str]:
    """
    Get all entities in the delegation chain (including owner).

    Args:
        passport: The passport to check

    Returns:
        List of entity names
    """
    parties = [passport.human_owner]

    if passport.delegation_chain:
        for hop in passport.delegation_chain:
            if hop.from_entity not in parties:
                parties.append(hop.from_entity)
            if hop.to not in parties:
                parties.append(hop.to)

    return parties


def is_in_delegation_chain(passport: Passport, entity: str) -> bool:
    """
    Check if an entity is in the delegation chain.

    Args:
        passport: The passport to check
        entity: Entity name to look for

    Returns:
        True if entity is in the chain
    """
    if passport.human_owner == entity:
        return True

    if passport.delegation_chain:
        for hop in passport.delegation_chain:
            if hop.from_entity == entity or hop.to == entity:
                return True

    return False


def get_delegation_path(passport: Passport) -> str:
    """
    Get the delegation path as a readable string.

    Args:
        passport: The passport to check

    Returns:
        String like "owner -> agent1 -> agent2"
    """
    parts = [passport.human_owner]

    if passport.delegation_chain:
        for hop in passport.delegation_chain:
            parts.append(hop.to)

    return " -> ".join(parts)


def has_expired_delegation(passport: Passport) -> bool:
    """
    Check if any delegation in the chain has expired.

    Args:
        passport: The passport to check

    Returns:
        True if any delegation has expired
    """
    if not passport.delegation_chain:
        return False

    now = datetime.utcnow()
    return any(
        hop.expires_at is not None and hop.expires_at < now
        for hop in passport.delegation_chain
    )


def get_expired_delegations(passport: Passport) -> list[DelegationHop]:
    """
    Get expired delegations.

    Args:
        passport: The passport to check

    Returns:
        List of expired DelegationHop objects
    """
    if not passport.delegation_chain:
        return []

    now = datetime.utcnow()
    return [
        hop for hop in passport.delegation_chain
        if hop.expires_at is not None and hop.expires_at < now
    ]


def get_expiring_delegations(
    passport: Passport,
    within_days: int = 7,
) -> list[DelegationHop]:
    """
    Get delegations expiring soon (within specified days).

    Args:
        passport: The passport to check
        within_days: Number of days to look ahead

    Returns:
        List of expiring DelegationHop objects
    """
    if not passport.delegation_chain:
        return []

    now = datetime.utcnow()
    threshold = now + timedelta(days=within_days)

    return [
        hop for hop in passport.delegation_chain
        if hop.expires_at is not None and hop.expires_at > now and hop.expires_at <= threshold
    ]


# ============================================================================
# Delegation Scope Analysis
# ============================================================================

def get_effective_scope(passport: Passport) -> list[str]:
    """
    Get the effective scope (intersection of all delegated scopes).

    Args:
        passport: The passport to check

    Returns:
        List of effective scope strings
    """
    effective_scope = set(passport.scope)

    if passport.delegation_chain:
        for hop in passport.delegation_chain:
            if hop.scope and len(hop.scope) > 0:
                delegated_scope = set(hop.scope)
                effective_scope = effective_scope & delegated_scope

    return list(effective_scope)


def is_scope_permitted(passport: Passport, scope: str) -> bool:
    """
    Check if a specific scope is permitted through the delegation chain.

    Args:
        passport: The passport to check
        scope: The scope to check

    Returns:
        True if scope is permitted
    """
    return scope in get_effective_scope(passport)


def get_scope_restrictions(passport: Passport) -> dict:
    """
    Get scope restrictions applied by delegations.

    Args:
        passport: The passport to check

    Returns:
        Dict with originalScope, effectiveScope, and restrictedBy keys
    """
    original_scope = passport.scope
    current_scope = set(original_scope)
    restricted_by: list[dict] = []

    if passport.delegation_chain:
        for hop in passport.delegation_chain:
            if hop.scope and len(hop.scope) > 0:
                delegated_scope = set(hop.scope)
                removed_scopes = [s for s in current_scope if s not in delegated_scope]

                if removed_scopes:
                    restricted_by.append({
                        "hop": hop.model_dump(by_alias=True, mode="json"),
                        "removedScopes": removed_scopes,
                    })

                current_scope = current_scope & delegated_scope

    return {
        "originalScope": original_scope,
        "effectiveScope": list(current_scope),
        "restrictedBy": restricted_by,
    }


# ============================================================================
# Delegation Validation
# ============================================================================

def validate_delegation_chain(passport: Passport) -> dict:
    """
    Validate a delegation chain.

    Args:
        passport: The passport to validate

    Returns:
        Dict with 'valid' (bool) and 'issues' (list[str]) keys
    """
    issues: list[str] = []

    if not passport.delegation_chain or len(passport.delegation_chain) == 0:
        return {"valid": True, "issues": []}

    # Check for circular delegations
    seen = {passport.human_owner}
    for hop in passport.delegation_chain:
        if hop.to in seen:
            issues.append(f'Circular delegation detected: "{hop.to}" appears multiple times')
        seen.add(hop.to)

    # Check for broken chain (from should match previous to)
    expected_from = passport.human_owner
    for i, hop in enumerate(passport.delegation_chain):
        if hop.from_entity != expected_from:
            issues.append(
                f'Broken chain at hop {i + 1}: expected from "{expected_from}", '
                f'got "{hop.from_entity}"'
            )
        expected_from = hop.to

    # Check for expired delegations
    expired = get_expired_delegations(passport)
    if expired:
        issues.append(f"{len(expired)} delegation(s) have expired")

    # Check delegation timestamps are in order
    last_timestamp: Optional[datetime] = None
    for i, hop in enumerate(passport.delegation_chain):
        hop_time = hop.granted_at
        if last_timestamp and hop_time < last_timestamp:
            issues.append(f"Delegation timestamp out of order at hop {i + 1}")
        last_timestamp = hop_time

    return {
        "valid": len(issues) == 0,
        "issues": issues,
    }
