"""Cut over an existing generated repo from a local user store to Cloud auth."""

from __future__ import annotations

import json
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Protocol
from uuid import UUID

from src.domain.codegen.auth_users_adapter import install_auth_users_adapter
from src.domain.codegen.legacy_store_retire import retire_shadow_store_files
from src.domain.codegen.legacy_user_store import (
    DiscoveredUser,
    RepoScan,
    normalize_role,
    scan_generated_repo,
)
from src.domain.project_auth.repository import (
    ProjectAuthConfigRepository,
    ProjectAuthKeyRepository,
    ProjectAuthRoleRepository,
)
from src.domain.project_auth.service import ProjectAuthService
from src.domain.projects.models import Project
from src.integrations.cloud_control_client import CloudControlClient, get_cloud_client_for_workspace


class CloudUserDirectory(Protocol):
    async def upsert_app_user(
        self,
        project_id: str,
        *,
        email: str,
        role: str,
        email_verified: bool = True,
        user_metadata: dict[str, Any] | None = None,
    ) -> dict[str, Any]: ...

    async def list_app_users(self, project_id: str) -> list[dict[str, Any]]: ...

    async def preserve_legacy_app_user(self, project_id: str, user: dict, legacy: dict) -> dict: ...

    async def create_entity_records(
        self, project_id: str, entity_key: str, records: list[dict[str, Any]]
    ) -> list[dict[str, Any]]: ...

    async def list_entity_records(self, project_id: str, entity_key: str) -> list[dict[str, Any]]: ...


@dataclass
class ImportResult:
    email: str
    role: str
    status: str
    detail: str = ""


@dataclass
class CutoverReport:
    project_id: str
    project_name: str
    project_type: str
    scan: RepoScan
    ensured_auth: bool = False
    imported: list[ImportResult] = field(default_factory=list)
    imported_records: dict[str, int] = field(default_factory=dict)
    provisioned_entities: list[dict[str, Any]] = field(default_factory=list)
    adapter: dict[str, list[str]] | None = None
    retired_stores: list[str] = field(default_factory=list)
    dry_run: bool = True
    source_archive: str | None = None

    def as_dict(self) -> dict[str, Any]:
        return {
            "project_id": self.project_id,
            "project_name": self.project_name,
            "project_type": self.project_type,
            "dry_run": self.dry_run,
            "ensured_auth": self.ensured_auth,
            "scan": self.scan.as_dict(),
            "imported": [item.__dict__ for item in self.imported],
            "imported_records": self.imported_records,
            "provisioned_entities": self.provisioned_entities,
            "adapter": self.adapter,
            "retired_stores": self.retired_stores,
            "source_archive": self.source_archive,
        }


async def import_users_to_cloud(
    cloud: CloudUserDirectory,
    project_id: str,
    users: list[DiscoveredUser],
    *,
    dry_run: bool,
) -> list[ImportResult]:
    results: list[ImportResult] = []
    for user in users:
        if dry_run:
            results.append(ImportResult(user.email, user.role, "dry_run"))
            continue
        try:
            metadata = {"source": "legacy_repo_import"}
            if user.name:
                metadata["name"] = user.name
            created = await cloud.upsert_app_user(
                project_id,
                email=user.email,
                role=user.role,
                email_verified=bool((user.legacy_data or {}).get("email_verified", False)),
                user_metadata=metadata,
            )
            if not isinstance(created, dict) or not created.get("id"):
                raise ValueError("Cloud did not return an imported identity")
            if str(created.get("email", "")).strip().lower() != user.email.strip().lower():
                raise ValueError("Cloud returned a different identity")
            if created.get("role") != user.role:
                raise ValueError("Cloud did not preserve the imported role")
            if user.legacy_data is not None:
                created = await cloud.preserve_legacy_app_user(
                    project_id, created, user.legacy_data
                )
                if created.get("app_metadata", {}).get("p2hs_legacy_user") != user.legacy_data:
                    raise ValueError("Cloud did not preserve legacy user fields")
            results.append(ImportResult(user.email, user.role, "upserted"))
        except Exception:
            results.append(
                ImportResult(user.email, user.role, "error", "Cloud identity import failed")
            )
    return results


_RECORD_META = {"id", "created_at", "updated_at", "createdAt", "updatedAt"}


def _record_create_payload(row: dict[str, Any], id_map: dict[str, str]) -> dict[str, Any]:
    raw = row.get("data") if isinstance(row.get("data"), dict) else row
    data = {}
    for key, value in raw.items():
        if key in _RECORD_META:
            continue
        data[key] = id_map.get(value, value) if isinstance(value, str) else value
    return {
        "data": data,
        "metadata": {"p2hs_legacy_record": {"id": row["id"]}},
    }


async def import_records_to_cloud(
    cloud: CloudUserDirectory,
    project_id: str,
    collections: dict[str, list[dict[str, Any]]],
    *,
    dry_run: bool,
) -> dict[str, int]:
    """Create Cloud records and rewrite relation IDs using the import order given."""
    imported: dict[str, int] = {}
    id_map: dict[str, str] = {}
    for entity, rows in collections.items():
        if not rows:
            imported[entity] = 0
            continue
        if dry_run:
            imported[entity] = len(rows)
            continue
        payloads = [_record_create_payload(row, id_map) for row in rows]
        created = await cloud.create_entity_records(project_id, entity, payloads)
        if len(created) != len(rows):
            raise ValueError("Cloud did not create every imported record")
        for source, actual in zip(rows, created, strict=True):
            new_id = str(actual["id"])
            id_map[str(source["id"])] = new_id
            legacy = (actual.get("metadata") or {}).get("p2hs_legacy_record")
            if isinstance(legacy, dict) and legacy.get("id") not in {None, source["id"]}:
                raise ValueError("Cloud did not preserve legacy record IDs")
        live = await cloud.list_entity_records(project_id, entity)
        if len(live) < len(rows):
            raise ValueError("Cloud record read-back verification failed")
        imported[entity] = len(rows)
    return imported


async def cut_over_generated_repo(
    *,
    db,
    project: Project,
    repo_path: str,
    import_dry_run: bool,
    adapter_dry_run: bool,
    ensure_auth: bool,
    extra_users: list[DiscoveredUser] | None = None,
    cloud: CloudUserDirectory | None = None,
    data_export: str | None = None,
    retire_shadow_stores: bool = False,
) -> CutoverReport:
    """Scan a checkout; optionally provision auth, import identities, install BFF."""
    project_id = UUID(str(project.id))
    project_name = str(project.name)
    project_type = str(project.type)
    workspace_id = UUID(str(project.workspace_id))
    scan = scan_generated_repo(repo_path)
    exported_users = None
    exported_records: dict[str, list[dict[str, Any]]] = {}
    if data_export:
        from src.domain.codegen.legacy_data_export import load_record_export, load_user_export

        exported_users = load_user_export(data_export, str(project_id))
        exported_records = load_record_export(data_export, str(project_id))
    report = CutoverReport(
        project_id=str(project_id),
        project_name=project_name,
        project_type=project_type,
        scan=scan,
        dry_run=import_dry_run and adapter_dry_run and not ensure_auth,
    )
    if not adapter_dry_run:
        from src.domain.codegen.legacy_data_export import archive_legacy_checkout

        report.source_archive = str(archive_legacy_checkout(repo_path))
    # A lookup failure must not redirect migration to another Cloud instance.
    client = cloud
    if client is None and (not import_dry_run or ensure_auth):
        client = await get_cloud_client_for_workspace(workspace_id, db)
        if not client._is_configured():
            raise ValueError("The project's Cloud instance is not configured")

    if ensure_auth:
        try:
            service = ProjectAuthService(
                config_repo=ProjectAuthConfigRepository(db),
                key_repo=ProjectAuthKeyRepository(db),
                role_repo=ProjectAuthRoleRepository(db),
                cloud_client=(
                    client if isinstance(client, CloudControlClient) else CloudControlClient()
                ),
            )
            await service.ensure_default_auth(project_id)
            report.ensured_auth = True
        except Exception as exc:
            report.ensured_auth = False
            if db is not None:
                try:
                    await db.rollback()
                except Exception:
                    pass
            raise ValueError("Cloud auth setup failed; local routes have been preserved") from exc

    if not import_dry_run:
        try:
            from src.domain.codegen.provision_manifest import installed_pack_entity_keys

            protected = await installed_pack_entity_keys(db, project_id) if db is not None else set()
            report.provisioned_entities = await _provision_declared_entities(
                client, str(project_id), repo_path, protected_entity_keys=protected
            )
        except Exception as exc:
            raise ValueError(
                "Cloud entity provision failed; local routes have been preserved"
            ) from exc

    users = merge_cutover_identities(scan.users, exported_users, extra_users)
    users = await _identities_for_cutover(db, project, users)
    try:
        report.imported = await import_users_to_cloud(
            client,
            str(project_id),
            users,
            dry_run=import_dry_run,
        )
    except Exception:
        report.imported = [
            ImportResult(user.email, user.role, "error", "cloud import failed") for user in users
        ]

    if exported_records:
        try:
            report.imported_records = await import_records_to_cloud(
                client,
                str(project_id),
                exported_records,
                dry_run=import_dry_run,
            )
        except Exception:
            raise ValueError("Cloud record import failed; local routes have been preserved") from None

    if not adapter_dry_run:
        if import_dry_run:
            raise ValueError("Route cutover requires a completed Cloud import")
        if any(item.status != "upserted" for item in report.imported):
            raise ValueError("Cloud import failed; local routes have been preserved")
        if exported_records and any(
            report.imported_records.get(entity) != len(rows)
            for entity, rows in exported_records.items()
        ):
            raise ValueError("Cloud record import failed; local routes have been preserved")
        live_users = await client.list_app_users(str(project_id))
        by_email = {str(user.get("email", "")).strip().lower(): user for user in live_users}
        for user in users:
            actual = by_email.get(user.email.strip().lower())
            if not actual or actual.get("role") != user.role:
                raise ValueError(
                    "Cloud read-back verification failed; local routes have been preserved"
                )
            if user.legacy_data is not None:
                if actual.get("app_metadata", {}).get("p2hs_legacy_user") != user.legacy_data:
                    raise ValueError(
                        "Cloud legacy field verification failed; local routes have been preserved"
                    )
                if (actual.get("status") == "active") != user.legacy_data["is_active"]:
                    raise ValueError(
                        "Cloud account status verification failed; local routes have been preserved"
                    )

    if adapter_dry_run:
        report.adapter = {
            "written": [],
            "backed_up": scan.local_route_files,
            "skipped": [],
        }
    else:
        report.adapter = install_auth_users_adapter(
            repo_path,
            project_type,
            replace_local=True,
        )
        from src.domain.codegen.legacy_store_retire import install_cloud_only_record_service

        cloud_services = install_cloud_only_record_service(repo_path)
        report.adapter["written"] = list(report.adapter.get("written") or []) + cloud_services
        report.adapter["backed_up"] = list(report.adapter.get("backed_up") or []) + [
            f"{relative}.p2hs-local.bak" for relative in cloud_services
        ]
        if retire_shadow_stores:
            report.retired_stores = retire_shadow_store_files(repo_path)
        # Retired source may contain seed credentials: keep it in private backups,
        # never stage it alongside the new public app source.
        backup_root = Path(report.source_archive).parent / "retired-routes"
        retired_backups = list(report.adapter["backed_up"])
        retired_backups.extend(
            f"{relative}.p2hs-local.bak" for relative in report.retired_stores
        )
        for relative in retired_backups:
            original = (Path(repo_path) / relative).resolve()
            if not original.is_file():
                continue
            if not original.is_relative_to(Path(repo_path).resolve()):
                raise ValueError("Invalid retired route path")
            target = backup_root / relative
            target.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
            original.rename(target)
    return report


def merge_cutover_identities(
    scan_users: list[DiscoveredUser],
    exported_users: list[DiscoveredUser] | None,
    extra_users: list[DiscoveredUser] | None,
) -> list[DiscoveredUser]:
    """Live export overwrites git-scan rows; CLI extras overwrite both."""
    merged = {user.email: user for user in scan_users}
    if exported_users is not None:
        merged.update({user.email: user for user in exported_users})
    if extra_users:
        merged.update({user.email: user for user in extra_users})
    return list(merged.values())


async def _identities_for_cutover(db, project: Project, users: list[DiscoveredUser]) -> list[DiscoveredUser]:
    """Prefer workspace membership over git seed roles. Owner is the only super_admin."""
    from dataclasses import replace

    from src.domain.project_auth.staff_directory import (
        identities_for_workspace,
        remap_legacy_app_role,
    )
    from src.domain.workspaces.models import Workspace

    owner_email = None
    workspace_users: list[DiscoveredUser] = []
    if db is not None:
        try:
            workspace = await db.get(Workspace, project.workspace_id)
            identities = await identities_for_workspace(
                db, project.workspace_id, getattr(workspace, "owner_id", None)
            )
            owner_email = next((item.email for item in identities if item.is_owner), None)
            workspace_users = [
                DiscoveredUser(
                    email=item.email,
                    role=item.app_role,
                    name=item.name,
                    source="workspace",
                )
                for item in identities
            ]
        except Exception:
            owner_email = None
    merged: dict[str, DiscoveredUser] = {}
    for user in [*users, *workspace_users]:
        existing = merged.get(user.email)
        role = remap_legacy_app_role(user.email, user.role, owner_email)
        merged[user.email] = replace(
            user,
            role=role,
            name=user.name or (existing.name if existing else ""),
            legacy_data=(
                user.legacy_data
                if user.legacy_data is not None
                else (existing.legacy_data if existing else None)
            ),
        )
    return list(merged.values())


_LEGACY_FIELD_TYPES = {
    "text": "text",
    "number": "number",
    "boolean": "boolean",
    "date": "date",
    "datetime": "date",
    "json": "json",
    "select": "select",
    "multi_select": "multi_select",
    "relation": "relation",
    # File inputs persist as canonical file_id text references. Cloud does
    # not gain a native file value type; the semantic overlay drives the
    # platform attachment projection.
    "file": "text",
}


def _legacy_entity_declarations(repo_path: str) -> list[dict[str, Any]]:
    """Map p2hs/entities.json (legacy slug/name fields) to provision declarations."""
    path = Path(repo_path) / "p2hs" / "entities.json"
    if not path.is_file():
        return []
    try:
        data = json.loads(path.read_text(encoding="utf-8-sig"))
    except (OSError, json.JSONDecodeError):
        return []
    declarations: list[dict[str, Any]] = []
    for entity in data.get("entities") or []:
        if not isinstance(entity, dict):
            continue
        key = str(entity.get("key") or entity.get("slug") or "").strip()
        if not key:
            continue
        fields: list[dict[str, Any]] = []
        for raw in entity.get("fields") or []:
            if not isinstance(raw, dict):
                continue
            field_key = str(raw.get("key") or raw.get("name") or "").strip()
            raw_type = str(raw.get("type") or "text")
            field_type = _LEGACY_FIELD_TYPES.get(raw_type)
            if not field_key or not field_type:
                continue
            field: dict[str, Any] = {
                "key": field_key,
                "label": str(raw.get("label") or field_key.replace("_", " ").title()),
                "type": field_type,
                "required": bool(raw.get("required")),
                "unique": bool(raw.get("unique")),
            }
            if raw_type == "file":
                field["semantic_type"] = "file_reference"
            if field_type in {"select", "multi_select"} and raw.get("options"):
                field["options"] = list(raw["options"])
            if field_type == "relation" and raw.get("relation_entity_key"):
                field["relation_entity_key"] = str(raw["relation_entity_key"])
            fields.append(field)
        if not fields:
            continue
        declarations.append(
            {
                "key": key,
                "label": str(entity.get("label") or entity.get("name") or key),
                "rls_enabled": bool(entity.get("rls_enabled")),
                "fields": fields,
            }
        )
    return declarations


async def _provision_declared_entities(
    client,
    project_id: str,
    repo_path: str,
    *,
    protected_entity_keys: set[str] | None = None,
) -> list[dict[str, Any]]:
    """Create Cloud entities from provision.json, then legacy entities.json."""
    from src.domain.codegen.provision_manifest import (
        load_manifest_from_repo_path,
        provision_entities,
    )

    if client is None:
        return []
    entities: list[dict[str, Any]] = []
    try:
        manifest = load_manifest_from_repo_path(repo_path)
        entities = list((manifest or {}).get("entities") or [])
    except Exception:
        entities = []
    if not entities:
        entities = _legacy_entity_declarations(repo_path)
    if not entities:
        return []
    return await provision_entities(
        client, project_id, entities, protected_entity_keys=protected_entity_keys
    )


async def ensure_cloud_auth_for_project(db, project_id: UUID) -> None:
    """Provision Cloud auth config/keys/owner. Non-fatal callers should catch."""
    from src.domain.projects.repository import ProjectRepository

    project = await ProjectRepository(db).get_by_id(project_id)
    if not project:
        return
    try:
        client = await get_cloud_client_for_workspace(project.workspace_id, db)
    except Exception:
        client = CloudControlClient()
    service = ProjectAuthService(
        config_repo=ProjectAuthConfigRepository(db),
        key_repo=ProjectAuthKeyRepository(db),
        role_repo=ProjectAuthRoleRepository(db),
        cloud_client=client,
    )
    await service.ensure_default_auth(project_id)


async def import_legacy_users_for_project(
    db,
    project_id: UUID,
    users: list[DiscoveredUser],
) -> list[ImportResult]:
    from src.domain.projects.repository import ProjectRepository

    project = await ProjectRepository(db).get_by_id(project_id)
    if not project:
        return [ImportResult(u.email, u.role, "error", "project not found") for u in users]
    try:
        client = await get_cloud_client_for_workspace(project.workspace_id, db)
    except Exception:
        client = CloudControlClient()
    return await import_users_to_cloud(client, str(project_id), users, dry_run=False)


def discovered_users_from_payload(raw: list[dict[str, Any]] | None) -> list[DiscoveredUser]:
    found: list[DiscoveredUser] = []
    for item in raw or []:
        if not isinstance(item, dict):
            continue
        email = str(item.get("email") or "").strip().lower()
        if "@" not in email:
            continue
        found.append(
            DiscoveredUser(
                email=email,
                role=normalize_role(str(item.get("role") or "")),
                name=str(item.get("name") or "").strip(),
                source="executor",
            )
        )
    return found
