"""Repository adapter between UUID-owned state and the legacy service facade.

This module is deliberately headless.  The loader remains the authority for
SSH projections; this adapter only applies the accepted identity matcher and
projects the resulting UUID state into the temporary alias-shaped
``ConnectionFileState`` consumed by ``ConnectionService``.
"""

from __future__ import annotations

from dataclasses import replace
from typing import Callable, Mapping, Optional, Sequence, Tuple

from .identity_reconciliation import (
    ConnectionIdentityProjection,
    IdentityRegistryEntry,
    Match,
    MatchReason,
    reconcile_identities,
)
from .identity_state_v2 import (
    ConnectionReference,
    IdentityStateV2,
    PendingAmbiguity,
    PersistedIdentity,
    ReferenceKind,
    UuidGroupState,
    migrate_v1_state,
    new_uuid4,
)
from .models import ConnectionRecord
from .state_file import ConnectionFileState, GroupFileState


def projections_from_records(
    records: Sequence[ConnectionRecord],
) -> Tuple[ConnectionIdentityProjection, ...]:
    """Capture loader evidence before records enter ``ConnectionService``."""

    return tuple(
        ConnectionIdentityProjection.from_record(record, declaration_order=index)
        for index, record in enumerate(records)
        if record.protocol == "ssh"
    )


def _identity_entry_map(state: IdentityStateV2) -> dict[str, PersistedIdentity]:
    return {identity.uuid: identity for identity in state.identities}


def _active_reference(uuid: str) -> ConnectionReference:
    return ConnectionReference(ReferenceKind.SSH_UUID, uuid)


def _placement_values(
    groups_in: Sequence[UuidGroupState],
    root_in: Sequence[ConnectionReference],
    active_uuids: set[str],
    add_root: Sequence[str] = (),
) -> tuple[Tuple[UuidGroupState, ...], Tuple[ConnectionReference, ...]]:
    """Remove retired references while preserving UUID-owned placement."""

    groups = []
    grouped: set[ConnectionReference] = set()
    for group in groups_in:
        members = []
        seen = set()
        for reference in group.members:
            if reference.kind is ReferenceKind.SSH_UUID and reference.value not in active_uuids:
                continue
            if reference in seen:
                continue
            seen.add(reference)
            members.append(reference)
            grouped.add(reference)
        groups.append(replace(group, members=tuple(members)))

    root = []
    seen_root = set()
    for reference in root_in:
        if reference.kind is ReferenceKind.SSH_UUID and reference.value not in active_uuids:
            continue
        if reference in grouped or reference in seen_root:
            continue
        seen_root.add(reference)
        root.append(reference)
    for uuid in add_root:
        reference = _active_reference(uuid)
        if reference not in grouped and reference not in seen_root:
            root.append(reference)
            seen_root.add(reference)
    for uuid in sorted(active_uuids):
        reference = _active_reference(uuid)
        if reference not in grouped and reference not in seen_root:
            root.append(reference)
            seen_root.add(reference)
    return tuple(groups), tuple(root)


def _with_current_placements(
    state: IdentityStateV2,
    *,
    active_uuids: set[str],
    add_root: Sequence[str] = (),
) -> IdentityStateV2:
    groups, root = _placement_values(
        state.groups, state.root_connections, active_uuids, add_root
    )
    return replace(state, groups=groups, root_connections=root)


def migrate_v1_to_identity_state(
    file_state: ConnectionFileState,
    projections: Sequence[ConnectionIdentityProjection],
    *,
    ssh_config_revision: str,
    uuid_factory: Callable[[], str] = new_uuid4,
) -> IdentityStateV2:
    state, _report = migrate_v1_state(
        file_state,
        projections,
        ssh_config_revision=ssh_config_revision,
        uuid_factory=uuid_factory,
    )
    return state


def reconcile_identity_state(
    state: IdentityStateV2,
    projections: Sequence[ConnectionIdentityProjection],
    *,
    ssh_config_revision: str,
    uuid_factory: Callable[[], str] = new_uuid4,
    explicit_continuity: Mapping[str, str] = (),
) -> IdentityStateV2:
    """Apply one frozen reconciliation pass to a persisted v2 snapshot.

    ``explicit_continuity`` maps an old alias to a new alias for an operation
    SSH Pilot itself performed.  Those pairs bypass heuristic evidence while
    all remaining candidates use the accepted matcher unchanged.
    """

    if type(ssh_config_revision) is not str or not ssh_config_revision.strip():
        raise ValueError("SSH config revision must be a non-empty string")
    old_entries = tuple(
        IdentityRegistryEntry(
            uuid=identity.uuid,
            display_name=identity.display_name,
            projection=identity.projection,
            tombstone=identity.tombstone,
        )
        for identity in state.identities
    )
    by_old_alias = {
        identity.projection.alias: identity
        for identity in state.identities
        if not identity.tombstone
    }
    by_new_alias = {projection.alias: projection for projection in projections}
    explicit_matches: list[Match] = []
    explicit_old_uuids: set[str] = set()
    explicit_new_aliases: set[str] = set()
    for old_alias, new_alias in dict(explicit_continuity).items():
        identity = by_old_alias.get(old_alias)
        projection = by_new_alias.get(new_alias)
        if identity is None or projection is None:
            raise ValueError("explicit identity continuity does not name current records")
        if identity.uuid in explicit_old_uuids or new_alias in explicit_new_aliases:
            raise ValueError("explicit identity continuity contains duplicates")
        explicit_old_uuids.add(identity.uuid)
        explicit_new_aliases.add(new_alias)
        explicit_matches.append(
            Match(
                IdentityRegistryEntry(
                    uuid=identity.uuid,
                    display_name=identity.display_name,
                    projection=identity.projection,
                ),
                projection,
                MatchReason.EXPLICIT_IN_APP_CONTINUITY,
            )
        )

    remaining_old = tuple(entry for entry in old_entries if entry.uuid not in explicit_old_uuids)
    remaining_new = tuple(
        projection for projection in projections if projection.alias not in explicit_new_aliases
    )
    result = reconcile_identities(
        remaining_old,
        remaining_new,
        uuid_factory=uuid_factory,
    )
    matches = tuple(explicit_matches) + result.matched
    deleted_uuids = {entry.uuid for entry in result.deleted}
    ambiguous_uuids = {
        entry.uuid for item in result.ambiguous for entry in item.old
    }
    created = {entry.uuid: entry for entry in result.created}
    next_generation = state.sidecar_generation
    identities = []
    for identity in state.identities:
        if identity.tombstone:
            identities.append(identity)
            continue
        match = next((item for item in matches if item.old.uuid == identity.uuid), None)
        if match is not None:
            identities.append(replace(identity, projection=match.new_projection, tombstone=False))
        elif identity.uuid in ambiguous_uuids:
            identities.append(identity)
        elif identity.uuid in deleted_uuids:
            identities.append(
                replace(identity, tombstone=True, retired_generation=next_generation + 1)
            )
        else:
            # A defensive invariant: every old active identity is matched,
            # ambiguous, or deleted by the frozen matcher.
            raise ValueError("reconciliation did not account for an active identity")
    identities.extend(
        PersistedIdentity(
            uuid=entry.uuid,
            display_name=entry.display_name or entry.projection.alias,
            projection=entry.projection,
        )
        for entry in created.values()
    )
    active_uuids = {identity.uuid for identity in identities if not identity.tombstone}
    pending = tuple(
        PendingAmbiguity(
            ssh_config_revision=ssh_config_revision,
            old_uuids=tuple(entry.uuid for entry in item.old),
            new_projections=item.new,
        )
        for item in result.ambiguous
    )
    groups, root = _placement_values(
        state.groups,
        state.root_connections,
        active_uuids,
        tuple(created),
    )
    candidate = replace(
        state,
        identities=tuple(identities),
        groups=groups,
        root_connections=root,
        pending_ambiguities=pending,
        observed_ssh_revision=ssh_config_revision,
        last_reconciled_ssh_revision=(
            state.last_reconciled_ssh_revision if pending else ssh_config_revision
        ),
    )

    # A v1 reference that was quarantined because it was temporarily absent
    # may safely be restored when that exact alias later becomes a newly
    # created SSH identity.  This is placement recovery, not tombstone
    # resurrection: quarantined aliases never had an active UUID.
    created_by_alias = {
        identity.projection.alias: identity.uuid
        for identity in candidate.identities
        if not identity.tombstone
        and identity.uuid in created
    }
    if created_by_alias and candidate.legacy_orphans:
        groups = list(candidate.groups)
        roots = list(candidate.root_connections)
        metadata = dict(candidate.metadata)
        remaining_orphans = []
        changed_by_orphans = False
        for orphan in candidate.legacy_orphans:
            uuid = created_by_alias.get(orphan.alias)
            if uuid is None:
                remaining_orphans.append(orphan)
                continue
            reference = _active_reference(uuid)
            if orphan.kind == "metadata":
                metadata[uuid] = orphan.values
                changed_by_orphans = True
                continue
            if orphan.kind == "group_member":
                group_index = next(
                    (index for index, group in enumerate(groups)
                     if group.id == orphan.group_id),
                    None,
                )
                if group_index is None:
                    remaining_orphans.append(orphan)
                    continue
                members = [item for item in groups[group_index].members if item != reference]
                insert_at = min(orphan.original_index or 0, len(members))
                members.insert(insert_at, reference)
                groups[group_index] = replace(
                    groups[group_index], members=tuple(members)
                )
                roots = [item for item in roots if item != reference]
                changed_by_orphans = True
                continue
            if any(reference in group.members for group in groups):
                remaining_orphans.append(orphan)
                continue
            roots = [item for item in roots if item != reference]
            insert_at = min(orphan.original_index or 0, len(roots))
            roots.insert(insert_at, reference)
            changed_by_orphans = True
        if changed_by_orphans:
            candidate = replace(
                candidate,
                groups=tuple(groups),
                root_connections=tuple(roots),
                metadata=metadata,
                legacy_orphans=tuple(remaining_orphans),
            )
    if candidate == state:
        return state
    return replace(candidate, sidecar_generation=state.sidecar_generation + 1)


def state_to_service_file_state(
    state: IdentityStateV2,
    records: Sequence[ConnectionRecord],
) -> ConnectionFileState:
    """Project UUID-owned placements/metadata to the legacy service shape."""

    alias_by_uuid = {
        identity.uuid: identity.projection.alias
        for identity in state.identities
        if not identity.tombstone
    }
    current_aliases = {record.id for record in records if record.protocol == "ssh"}

    def project(reference: ConnectionReference) -> Optional[str]:
        if reference.kind is ReferenceKind.NON_SSH_ID:
            return reference.value
        alias = alias_by_uuid.get(reference.value)
        return alias if alias in current_aliases else None

    groups = []
    grouped = set()
    for group in state.groups:
        members = []
        for reference in group.members:
            alias = project(reference)
            if alias is not None and alias not in members:
                members.append(alias)
                grouped.add(alias)
        groups.append(
            GroupFileState(
                id=group.id,
                name=group.name,
                parent_id=group.parent_id,
                order=group.order,
                color=group.color,
                connection_ids=tuple(members),
            )
        )
    roots = []
    for reference in state.root_connections:
        alias = project(reference)
        if alias is not None and alias not in grouped and alias not in roots:
            roots.append(alias)
    # Ambiguous projections remain launchable in the service facade but have
    # no UUID-owned placement.  Root placement here is runtime-only.
    roots.extend(
        record.id
        for record in records
        if record.protocol == "ssh"
        and record.id not in grouped
        and record.id not in roots
    )
    metadata = {}
    for uuid, values in state.metadata.items():
        alias = alias_by_uuid.get(uuid)
        if alias in current_aliases:
            metadata[alias] = values
    return ConnectionFileState(
        version=1,
        non_ssh_connections=tuple(item for item in state.non_ssh_connections),
        groups=tuple(groups),
        root_connections=tuple(roots),
        metadata=metadata,
    )
