"""Project-scoped hosting profile.

A P2HS application is defined independently of where it runs. The profile
describes what a host can execute, build, route, and migrate. Provider
modules register their profile; generic codegen, deploy, and migration
resolve it from the project's persisted selection.

Cloudflare W4P is the platform default for new projects. Cloudflare Pages is a
legacy host: existing projects keep it until an explicit migration, and it
remains a valid migration source and target. Lifecycle is not compatibility.
A legacy host is not selected for new projects unless that choice is authorized.
"""
from __future__ import annotations

from collections.abc import Callable, Mapping
from dataclasses import dataclass
from typing import Literal

from src.domain.hosting.project_provider import W4P_STATIC

PROFILE_CONTRACT_VERSION = 1

CompatibilityStatus = Literal["compatible", "compatible_with_transformation", "blocked"]
ProviderLifecycle = Literal["default", "supported", "legacy", "deprecated", "disabled"]
WorkflowRenderer = Callable[["HostingProfile", str, str, object], str]
ProjectionFactory = Callable[[], object]

_PAGES = "cloudflare_pages"
_ALIASES = {
    "w4p": W4P_STATIC,
    "cloudflare_w4p": W4P_STATIC,
    W4P_STATIC: W4P_STATIC,
    "cloudflare": _PAGES,
    _PAGES: _PAGES,
}
_STATIC_FRAMEWORKS = frozenset({"html", "react_app", "nuxt_app"})

_REGISTRY: dict[str, HostingProfile] = {}
_WORKFLOW_RENDERERS: dict[str, WorkflowRenderer] = {}
_PROJECTION_FACTORIES: dict[str, ProjectionFactory] = {}


class HostingProfileError(ValueError):
    """The requested hosting profile is missing or cannot perform the operation."""


@dataclass(frozen=True)
class HostingProfile:
    """Capabilities and constraints for one hosting provider. Not merely its name."""

    provider_id: str
    profile_version: int
    lifecycle: ProviderLifecycle
    migration_source: bool
    migration_target: bool
    supported_frameworks: frozenset[str]
    static_assets: bool
    ssr_supported: bool
    server_function_surface: str
    bff_strategy: str
    execution_mode: str
    runtime_restrictions: tuple[str, ...]
    build_outputs: Mapping[str, tuple[str, str]]
    secret_bindings: tuple[str, ...]
    preview_supported: bool
    routing_strategy: str
    ssl_lifecycle: str
    health_check: str
    rollback_supported: bool
    workflow_id: str
    projection_id: str
    migration_requirements: tuple[str, ...]


@dataclass(frozen=True)
class ApplicationHostingRequirements:
    """What the application needs from whichever host runs it."""

    framework: str
    requires_ssr: bool = False
    requires_server_functions: bool = True
    requires_bff: bool = True
    requires_custom_domains: bool = True


@dataclass(frozen=True)
class CompatibilityResult:
    status: CompatibilityStatus
    reasons: tuple[str, ...] = ()
    remediations: tuple[str, ...] = ()


@dataclass(frozen=True)
class HostingMigrationPlan:
    """Dry-run plan. Constructing it does not change the active provider.

    ``platform_default`` is where new projects go. ``active_provider`` is the
    project that is running now. ``migration_target`` is only the destination
    being prepared. The active provider stays put until a verified cutover.
    """

    platform_default: str
    active_provider: str
    migration_target: str
    compatibility: CompatibilityResult
    stages: tuple[str, ...]
    dry_run: bool = True
    mutates_production: bool = False
    commits_active_provider: bool = False


_MIGRATION_STAGES = (
    "discover",
    "resolve_profiles",
    "evaluate_compatibility",
    "snapshot_manifest",
    "prepare_runtime",
    "detect_conflicts",
    "prepare_bindings",
    "deploy_preview",
    "readiness",
    "prepare_routing",
    "activate_traffic",
    "verify",
    "commit_state",
    "retain_rollback",
)


def canonical_provider_id(provider_id: str | None) -> str:
    key = (provider_id or "").strip().lower()
    return _ALIASES.get(key, key)


def register_hosting_profile(profile: HostingProfile) -> None:
    _REGISTRY[profile.provider_id] = profile


def unregister_hosting_profile(provider_id: str) -> None:
    _REGISTRY.pop(canonical_provider_id(provider_id), None)


def register_workflow_renderer(workflow_id: str, renderer: WorkflowRenderer) -> None:
    _WORKFLOW_RENDERERS[workflow_id] = renderer


def unregister_workflow_renderer(workflow_id: str) -> None:
    _WORKFLOW_RENDERERS.pop(workflow_id, None)


def register_projection_factory(projection_id: str, factory: ProjectionFactory) -> None:
    _PROJECTION_FACTORIES[projection_id] = factory


def find_hosting_profile(provider_id: str | None) -> HostingProfile | None:
    return _REGISTRY.get(canonical_provider_id(provider_id))


def get_hosting_profile(provider_id: str | None) -> HostingProfile:
    profile = find_hosting_profile(provider_id)
    if profile is None:
        raise HostingProfileError(f"unregistered hosting provider {provider_id}")
    return profile


def platform_default_profile() -> HostingProfile:
    """The single preferred host for new projects. Not a lock on existing ones."""

    defaults = [profile for profile in _REGISTRY.values() if profile.lifecycle == "default"]
    if len(defaults) != 1:
        raise HostingProfileError("exactly one hosting profile must be the platform default")
    return defaults[0]


def provider_for_new_project(
    framework: str,
    explicit: str | None = None,
    *,
    authorized_override: bool = False,
) -> str | None:
    """Default new static apps to W4P. Legacy hosts need an explicit authorization."""

    if framework not in _STATIC_FRAMEWORKS:
        return None
    if not (explicit or "").strip():
        return platform_default_profile().provider_id
    profile = get_hosting_profile(explicit)
    if profile.lifecycle == "disabled":
        raise HostingProfileError(f"{profile.provider_id} is disabled")
    restricted = profile.lifecycle in {"legacy", "deprecated"}
    if restricted and not authorized_override:
        raise HostingProfileError(
            f"{profile.provider_id} is {profile.lifecycle} and is not selectable for new projects"
        )
    return profile.provider_id


def resolve_hosting_profile(
    primary_hosting_provider: str | None,
    *,
    default_provider: str | None,
) -> HostingProfile:
    """Persisted project selection wins. The default applies only when it is empty."""

    explicit = (primary_hosting_provider or "").strip()
    if explicit:
        return get_hosting_profile(explicit)
    return get_hosting_profile(default_provider)


def render_deploy_workflow(
    profile: HostingProfile,
    project_type: str,
    project_ref: str,
    settings: object,
) -> str:
    renderer = _WORKFLOW_RENDERERS.get(profile.workflow_id)
    if renderer is None:
        raise HostingProfileError(f"no workflow renderer for {profile.workflow_id}")
    return renderer(profile, project_type, project_ref, settings)


def projection_for_provider(provider_id: str | None) -> object:
    from src.domain.hosting.runtime_projection import StubRuntimeProjection

    profile = find_hosting_profile(provider_id)
    if profile is not None:
        projection_id = profile.projection_id
    else:
        projection_id = canonical_provider_id(provider_id)
    factory = _PROJECTION_FACTORIES.get(projection_id)
    if factory is None:
        return StubRuntimeProjection()
    return factory()


def evaluate_hosting_compatibility(
    source: HostingProfile,
    target: HostingProfile,
    requirements: ApplicationHostingRequirements,
) -> CompatibilityResult:
    """Framework support alone is not compatibility. Execution requirements are checked."""

    reasons: list[str] = []
    remediations: list[str] = []
    blocked = False
    transformation = source.provider_id != target.provider_id

    if requirements.framework not in target.supported_frameworks:
        blocked = True
        reasons.append(f"{target.provider_id} does not support {requirements.framework}")
        remediations.append("choose a host whose profile lists this framework")

    if requirements.requires_ssr and not target.ssr_supported:
        if target.static_assets and requirements.framework == "nuxt_app":
            transformation = True
            reasons.append("Nuxt SSR is served as a static shell plus server functions")
            remediations.append("generate the static shell; do not execute Nitro server routes")
        else:
            blocked = True
            reasons.append(f"{target.provider_id} cannot execute SSR for {requirements.framework}")
            remediations.append("remove the SSR requirement or choose a host that supports it")

    if requirements.requires_server_functions and target.server_function_surface == "none":
        blocked = True
        reasons.append(f"{target.provider_id} has no server-function surface")
        remediations.append("choose a host that can run the BFF or remove server functions")

    if requirements.requires_bff and target.bff_strategy == "none":
        blocked = True
        reasons.append(f"{target.provider_id} cannot host the BFF")
        remediations.append("keep Cloud capability calls behind a host that exposes a BFF")

    if requirements.requires_custom_domains and target.routing_strategy == "none":
        blocked = True
        reasons.append(f"{target.provider_id} cannot bind custom domains")
        remediations.append("choose a host with a domain and SSL strategy")

    if not blocked and transformation:
        reasons.append("deployment, routing, and managed runtime files change")
        if target.runtime_restrictions:
            reasons.append("target runtime restrictions: " + ", ".join(target.runtime_restrictions))
        remediations.append("preview the destination before production traffic moves")

    if blocked:
        status: CompatibilityStatus = "blocked"
    elif transformation:
        status = "compatible_with_transformation"
    else:
        status = "compatible"
    return CompatibilityResult(status, tuple(reasons), tuple(remediations))


def _eligibility_block(
    source: HostingProfile,
    target: HostingProfile,
) -> CompatibilityResult | None:
    reasons: list[str] = []
    remediations: list[str] = []
    if source.lifecycle == "disabled" or not source.migration_source:
        reasons.append(f"{source.provider_id} cannot be a migration source")
        remediations.append("choose a host that is registered as a migration source")
    if target.lifecycle == "disabled" or not target.migration_target:
        reasons.append(f"{target.provider_id} cannot be a migration target")
        remediations.append("choose a host that is registered as a migration target")
    if not reasons:
        return None
    return CompatibilityResult("blocked", tuple(reasons), tuple(remediations))


def plan_hosting_migration(
    source_provider: str,
    target_provider: str,
    requirements: ApplicationHostingRequirements,
) -> HostingMigrationPlan:
    """Compatibility preflight. The active provider is not changed.

    Either registered host may be the source. W4P is not assumed to be the destination.
    """

    source = get_hosting_profile(source_provider)
    target = get_hosting_profile(target_provider)
    eligibility = _eligibility_block(source, target)
    compatibility = eligibility or evaluate_hosting_compatibility(source, target, requirements)
    if compatibility.status == "blocked":
        stages = _MIGRATION_STAGES[:3]
    else:
        stages = _MIGRATION_STAGES
    return HostingMigrationPlan(
        platform_default=platform_default_profile().provider_id,
        active_provider=source.provider_id,
        migration_target=target.provider_id,
        compatibility=compatibility,
        stages=stages,
    )


def _static_outputs(
    command_by_framework: Mapping[str, tuple[str, str]],
) -> Mapping[str, tuple[str, str]]:
    return dict(command_by_framework)


def _pages_projection() -> object:
    from src.domain.hosting.runtime_projection import LegacyPagesRuntimeProjection

    return LegacyPagesRuntimeProjection()


def _w4p_projection() -> object:
    from src.domain.hosting.runtime_projection import W4PSharedBFFRuntimeProjection

    return W4PSharedBFFRuntimeProjection()


def _register_builtin_profiles() -> None:
    register_projection_factory("pages_functions", _pages_projection)
    register_projection_factory("w4p_bff", _w4p_projection)
    pages_outputs = {
        "html": ("", ""),
        "react_app": ("npm run build", "dist"),
        "nuxt_app": ("npm run generate", ".output/public"),
    }
    register_hosting_profile(HostingProfile(
        provider_id=_PAGES,
        profile_version=PROFILE_CONTRACT_VERSION,
        lifecycle="legacy",
        migration_source=True,
        migration_target=True,
        supported_frameworks=_STATIC_FRAMEWORKS,
        static_assets=True,
        ssr_supported=False,
        server_function_surface="pages_functions",
        bff_strategy="pages_functions",
        execution_mode="static_and_bff",
        runtime_restrictions=(),
        build_outputs=_static_outputs(pages_outputs),
        secret_bindings=("CLOUDFLARE_API_TOKEN", "CLOUDFLARE_ACCOUNT_ID"),
        preview_supported=True,
        routing_strategy="pages_hostname",
        ssl_lifecycle="provider_managed",
        health_check="pages_deployment",
        rollback_supported=True,
        workflow_id=_PAGES,
        projection_id="pages_functions",
        migration_requirements=("managed_runtime_manifest", "preview_before_cutover"),
    ))
    register_hosting_profile(HostingProfile(
        provider_id=W4P_STATIC,
        profile_version=PROFILE_CONTRACT_VERSION,
        lifecycle="default",
        migration_source=True,
        migration_target=True,
        supported_frameworks=_STATIC_FRAMEWORKS,
        static_assets=True,
        ssr_supported=False,
        server_function_surface="worker_bundle",
        bff_strategy="project_worker",
        execution_mode="static_and_bff",
        runtime_restrictions=(
            "named_exports_only",
            "javascript_pages_functions_only",
        ),
        build_outputs=_static_outputs(pages_outputs),
        secret_bindings=("CLOUDFLARE_API_TOKEN", "CLOUDFLARE_ACCOUNT_ID"),
        preview_supported=True,
        routing_strategy="w4p_kv",
        ssl_lifecycle="platform_saas",
        health_check="worker_readiness",
        rollback_supported=True,
        workflow_id=W4P_STATIC,
        projection_id="w4p_bff",
        migration_requirements=(
            "managed_runtime_manifest",
            "preview_before_cutover",
            "named_export_server_functions",
        ),
    ))


_register_builtin_profiles()
