from __future__ import annotations

import json
import secrets
from datetime import UTC, datetime
from uuid import uuid4

from app.core.crypto import generate_secret_token_urlsafe, hash_opaque_token
from app.core.exceptions import DomainError
from app.modules.sync import repository as repo
from app.modules.sync.schemas import (
    AckChangesRequest,
    AckChangesResponse,
    ChangeEventIn,
    ChangeEventOut,
    ConflictDetail,
    ConflictSummary,
    HeartbeatRequest,
    HeartbeatResponse,
    PairConfirmRequest,
    PairConfirmResponse,
    PairStartRequest,
    PairStartResponse,
    PullChangesRequest,
    PullChangesResponse,
    PushChangesRequest,
    PushChangesResponse,
    ResolveConflictRequest,
    SyncCapabilitiesResponse,
)
from app.modules.sync.engine import SyncEngine
from app.modules.users.models import User


class SyncServiceError(DomainError):
    pass


def get_capabilities() -> SyncCapabilitiesResponse:
    return SyncCapabilitiesResponse()


def start_pairing(user: User, body: PairStartRequest) -> PairStartResponse:
    member = repo.get_member(user.id, body.enterprise_id)
    if not member or member.role != "admin":
        if not user.is_superuser:
            raise SyncServiceError("ENTERPRISE_ADMIN_ONLY")
    enterprise = repo.get_enterprise_by_id(body.enterprise_id)
    if not enterprise:
        raise SyncServiceError("ENTERPRISE_NOT_FOUND")
    code = f"{secrets.randbelow(900000) + 100000:06d}"
    session = repo.create_pairing_session(body.enterprise_id, code, hash_opaque_token(code))
    return PairStartResponse(session_id=session.id, code=code, expires_at=session.expires_at)


def confirm_pairing(body: PairConfirmRequest) -> PairConfirmResponse:
    code_hash = hash_opaque_token(body.code.strip())
    session = repo.get_pairing_session_by_code_hash(code_hash)
    if not session:
        raise SyncServiceError("INVALID_PAIRING_CODE")
    if session.expires_at.replace(tzinfo=UTC) < datetime.now(UTC):
        raise SyncServiceError("PAIRING_CODE_EXPIRED")
    hub = repo.create_farm_hub(
        session.enterprise_id,
        body.hub_name or f"Hub {body.hub_site_id[:8]}",
        body.hub_site_id,
        body.hub_url,
    )
    api_key = generate_secret_token_urlsafe(48)
    repo.create_hub_credential(hub.id, api_key)
    repo.confirm_pairing_session(session.id, hub.id)
    return PairConfirmResponse(
        farm_hub_id=hub.id,
        hub_site_id=hub.hub_site_id,
        api_key=api_key,
        enterprise_id=session.enterprise_id,
    )


def hub_heartbeat(hub, body: HeartbeatRequest) -> HeartbeatResponse:
    repo.update_hub_heartbeat(hub.farm_hub_id, body.wesp_version)
    return HeartbeatResponse(server_time=datetime.now(UTC))


def push_changes(hub, body: PushChangesRequest) -> PushChangesResponse:
    return SyncEngine(hub.enterprise_id, hub.hub_site_id).process_push(body.events, origin="hub")


def pull_changes(hub, body: PullChangesRequest) -> PullChangesResponse:
    return SyncEngine(hub.enterprise_id, hub.hub_site_id).process_pull(hub.farm_hub_id, body.cursor, body.limit)


def ack_changes(hub, body: AckChangesRequest) -> AckChangesResponse:
    return SyncEngine(hub.enterprise_id, hub.hub_site_id).process_ack(hub.farm_hub_id, body)


def list_conflicts_for_enterprise(enterprise_id: str) -> list[ConflictSummary]:
    rows = repo.list_conflicts(enterprise_id)
    return [
        ConflictSummary(
            id=row.id,
            table_name=row.table_name,
            record_id=row.record_id,
            farm_hub_id=row.farm_hub_id,
            status=row.status,
            created_at=row.created_at,
        )
        for row in rows
    ]


def get_conflict_detail(conflict_id: str, enterprise_id: str) -> ConflictDetail:
    row = repo.get_conflict(conflict_id)
    if not row or row.enterprise_id != enterprise_id:
        raise SyncServiceError("CONFLICT_NOT_FOUND")
    return ConflictDetail(
        id=row.id,
        table_name=row.table_name,
        record_id=row.record_id,
        farm_hub_id=row.farm_hub_id,
        status=row.status,
        created_at=row.created_at,
        orchestrator_snapshot=json.loads(row.orchestrator_snapshot_json or "{}"),
        hub_snapshot=json.loads(row.hub_snapshot_json or "{}"),
        held_event_ids=json.loads(row.held_event_ids_json or "[]"),
    )


def resolve_conflict(conflict_id: str, enterprise_id: str, user_id: str, body: ResolveConflictRequest) -> None:
    SyncEngine(enterprise_id, "orchestrator").resolve_conflict(conflict_id, user_id, body.resolution)


def get_sync_metrics(enterprise_id: str) -> dict:
    from sqlalchemy import func, select

    from app.core.database import session_scope
    from app.modules.sync.models import FarmHub, SyncConflict, SyncCursor, SyncEventLog, SyncOutbox

    with session_scope() as db:
        hubs = list(db.scalars(select(FarmHub).where(FarmHub.enterprise_id == enterprise_id)))
        outbox_pending = db.scalar(
            select(func.count())
            .select_from(SyncOutbox)
            .where(SyncOutbox.enterprise_id == enterprise_id, SyncOutbox.status == "pending")
        )
        conflicts_pending = db.scalar(
            select(func.count())
            .select_from(SyncConflict)
            .where(SyncConflict.enterprise_id == enterprise_id, SyncConflict.status == "pending")
        )
        max_seq = db.scalar(
            select(func.max(SyncEventLog.seq)).where(SyncEventLog.enterprise_id == enterprise_id)
        ) or 0
        hub_metrics = []
        for h in hubs:
            cursor = db.scalar(
                select(SyncCursor).where(
                    SyncCursor.farm_hub_id == h.id,
                    SyncCursor.direction == "inbound",
                )
            )
            lag = max(0, int(max_seq) - int(cursor.last_acked_seq if cursor else 0))
            hub_metrics.append(
                {
                    "farm_hub_id": h.id,
                    "name": h.name,
                    "hub_site_id": h.hub_site_id,
                    "status": h.status,
                    "last_seen": h.last_seen.isoformat() if h.last_seen else None,
                    "sync_lag": lag,
                }
            )
        return {
            "hubs": hub_metrics,
            "outbox_pending": int(outbox_pending or 0),
            "conflicts_pending": int(conflicts_pending or 0),
            "max_event_seq": int(max_seq),
        }
