"""Projects domain - Pydantic schemas."""
from __future__ import annotations
from datetime import datetime
from enum import Enum
from typing import Literal, Optional, Union
from uuid import UUID

from pydantic import BaseModel, Field, field_validator, model_validator


class ProjectType(str, Enum):
    REACT_APP = "react_app"
    NUXT_APP = "nuxt_app"
    HTML = "html"
    SHOPIFY_THEME = "shopify_theme"
    WORDPRESS_THEME = "wordpress_theme"
    SHOPIFY_APP = "shopify_app"
    AI_ASSIST_SHOPIFY = "ai_assist_shopify"
    AI_ASSIST_WORDPRESS = "ai_assist_wordpress"


class ExternalIndexAdapterSettings(BaseModel):
    """Core-owned, non-secret configuration for a Cloud search index adapter."""

    entity_key: str = Field(..., min_length=1, max_length=100, pattern=r"^[a-z0-9_*][a-z0-9_.-]*$")
    endpoint: str = Field(..., min_length=1, max_length=2048)
    connection_id: str = Field(..., min_length=1, max_length=120)
    timeout_ms: int = Field(default=1500, ge=250, le=5000)
    fallback_to_local: bool = True

    @field_validator("endpoint")
    @classmethod
    def _https_only(cls, value: str) -> str:
        value = value.strip()
        if not value.startswith("https://"):
            raise ValueError("external index endpoint must use HTTPS")
        return value


class ProjectStackSelectionMode(str, Enum):
    AUTO = "auto"
    PREFER = "prefer"
    REQUIRE = "require"


class ProjectGithubImportSource(BaseModel):
    """Import an existing customer GitHub repo into a P2HS managed project repo."""

    kind: Literal["github_import"] = "github_import"
    workspace_user_github_installation_id: UUID
    repo_full_name: str = Field(..., min_length=3, max_length=255)
    branch: Optional[str] = Field(default=None, max_length=100)
    root_dir: str = Field(default="/", max_length=255)
    import_env: bool = True

    @field_validator("repo_full_name")
    @classmethod
    def _repo_full_name(cls, value: str) -> str:
        v = value.strip()
        if "/" not in v or v.startswith("/") or v.endswith("/"):
            raise ValueError("repo_full_name must be in owner/repo form.")
        return v

    @field_validator("root_dir")
    @classmethod
    def _root_dir(cls, value: str) -> str:
        raw = (value or "/").strip().replace("\\", "/")
        if raw in ("", "."):
            return "/"
        raw = raw.strip("/")
        if ".." in raw.split("/"):
            raise ValueError("root_dir cannot contain '..'.")
        return raw or "/"


class ProjectGitlabImportSource(BaseModel):
    """Import an existing customer GitLab project into a P2HS managed project repo."""

    kind: Literal["gitlab_import"] = "gitlab_import"
    workspace_user_gitlab_connection_id: UUID
    repo_full_name: str = Field(..., min_length=3, max_length=255)
    branch: Optional[str] = Field(default=None, max_length=100)
    root_dir: str = Field(default="/", max_length=255)
    import_env: bool = True

    @field_validator("repo_full_name")
    @classmethod
    def _repo_full_name(cls, value: str) -> str:
        return ProjectGithubImportSource._repo_full_name(value)

    @field_validator("root_dir")
    @classmethod
    def _root_dir(cls, value: str) -> str:
        return ProjectGithubImportSource._root_dir(value)


class ProjectBitbucketImportSource(BaseModel):
    """Import an existing customer Bitbucket repo into a P2HS managed project repo."""

    kind: Literal["bitbucket_import"] = "bitbucket_import"
    workspace_user_bitbucket_connection_id: UUID
    repo_full_name: str = Field(..., min_length=3, max_length=255)
    branch: Optional[str] = Field(default=None, max_length=100)
    root_dir: str = Field(default="/", max_length=255)
    import_env: bool = True

    @field_validator("repo_full_name")
    @classmethod
    def _repo_full_name(cls, value: str) -> str:
        return ProjectGithubImportSource._repo_full_name(value)

    @field_validator("root_dir")
    @classmethod
    def _root_dir(cls, value: str) -> str:
        return ProjectGithubImportSource._root_dir(value)


class ProjectAzureDevopsImportSource(BaseModel):
    """Import an existing customer Azure DevOps repo into a P2HS managed project repo."""

    kind: Literal["azure_devops_import"] = "azure_devops_import"
    workspace_user_azure_connection_id: UUID
    repo_full_name: str = Field(..., min_length=3, max_length=255)
    branch: Optional[str] = Field(default=None, max_length=100)
    root_dir: str = Field(default="/", max_length=255)
    import_env: bool = True

    @field_validator("repo_full_name")
    @classmethod
    def _repo_full_name(cls, value: str) -> str:
        return ProjectGithubImportSource._repo_full_name(value)

    @field_validator("root_dir")
    @classmethod
    def _root_dir(cls, value: str) -> str:
        return ProjectGithubImportSource._root_dir(value)


class ProjectCreate(BaseModel):
    """Create project."""

    name: str
    discovery_requested: bool = False
    type: Optional[ProjectType] = None
    description: Optional[str] = None
    intent: Optional[str] = Field(
        default=None,
        description="Outcome-first description, e.g. 'marketing site', 'internal dashboard', or 'wordpress assistant'.",
    )
    preferred_stack: Optional[ProjectType] = None
    stack_selection_mode: ProjectStackSelectionMode = ProjectStackSelectionMode.AUTO
    template_id: Optional[UUID] = None
    source: Optional[
        Union[
            ProjectGithubImportSource,
            ProjectGitlabImportSource,
            ProjectBitbucketImportSource,
            ProjectAzureDevopsImportSource,
        ]
    ] = None

    @model_validator(mode="after")
    def _template_or_source(self):
        if self.template_id and self.source:
            raise ValueError("template_id cannot be combined with source import.")
        if (
            self.stack_selection_mode == ProjectStackSelectionMode.REQUIRE
            and self.preferred_stack is None
        ):
            raise ValueError("preferred_stack is required when stack_selection_mode=require.")
        if self.type and self.preferred_stack and self.stack_selection_mode == ProjectStackSelectionMode.REQUIRE:
            if self.type != self.preferred_stack:
                raise ValueError("type must match preferred_stack when stack_selection_mode=require.")
        # ProjectService intentionally supports name-only creation and resolves
        # the stack from its configured auto-selection fallback.  Keep this
        # request schema permissive so the service can perform that resolution
        # (and so source imports are not rejected before reaching the service).
        return self


class ProjectRecommendationRequest(BaseModel):
    """Resolve intent + optional power-user override into a concrete project type/template."""

    intent: Optional[str] = None
    type: Optional[ProjectType] = None
    preferred_stack: Optional[ProjectType] = None
    stack_selection_mode: ProjectStackSelectionMode = ProjectStackSelectionMode.AUTO
    template_id: Optional[UUID] = None

    @model_validator(mode="after")
    def _validate_request(self):
        if (
            self.stack_selection_mode == ProjectStackSelectionMode.REQUIRE
            and self.preferred_stack is None
        ):
            raise ValueError("preferred_stack is required when stack_selection_mode=require.")
        if not self.type and not self.template_id and not self.intent and not self.preferred_stack:
            raise ValueError("Provide at least one of type, template_id, intent, or preferred_stack.")
        return self


class ProjectTemplateCandidateResponse(BaseModel):
    template_id: UUID
    template_ref: str
    template_name: str
    framework: ProjectType
    score: int = Field(ge=0)
    matched_terms: list[str] = Field(default_factory=list)


class ProjectRecommendationResponse(BaseModel):
    resolved_project_type: ProjectType
    template_id: Optional[UUID] = None
    template_ref: Optional[str] = None
    template_name: Optional[str] = None
    template_selection_reason: Optional[str] = None
    template_candidates: list[ProjectTemplateCandidateResponse] = Field(default_factory=list)
    stack_selection_mode: ProjectStackSelectionMode
    preferred_stack: Optional[ProjectType] = None
    requested_intent: Optional[str] = None
    decision_reason: str
    warnings: list[str] = Field(default_factory=list)


class ProjectImportStart(BaseModel):
    """Start an existing-project import flow."""

    workspace_id: UUID
    provider: Literal["github", "gitlab", "bitbucket", "azure_devops"] = "github"
    redirect_base: Optional[str] = None
    azure_org_url: Optional[str] = None


class ProjectImportSourceEndpoint(BaseModel):
    method: Literal["GET", "POST"]
    path: str


class ProjectImportSourceOption(BaseModel):
    provider: str
    label: str
    enabled: bool
    icon: Optional[str] = None
    install_url: Optional[str] = None
    state: Optional[str] = None
    endpoints: dict[str, ProjectImportSourceEndpoint] = Field(default_factory=dict)

    @model_validator(mode="after")
    def _enabled_sources_expose_required_endpoints(self):
        if self.provider not in {"github", "gitlab", "bitbucket", "azure_devops"} or not self.enabled:
            return self
        endpoint_keys = set(self.endpoints)
        requirement_groups = [
            {"authorizations", "repository_options"},
        ]
        if self.provider == "github":
            requirement_groups[0].add("confirm_authorization")
            requirement_groups.append({"installations", "confirm_installation", "repositories"})
        elif self.provider == "gitlab":
            requirement_groups.append({"connections", "projects"})
        elif self.provider == "bitbucket":
            requirement_groups.append({"connections", "repositories"})
        elif self.provider == "azure_devops":
            requirement_groups[0].add("connect_url")
            requirement_groups.append({"connections", "repositories", "connect_url"})

        if not any(group.issubset(endpoint_keys) for group in requirement_groups):
            preferred = requirement_groups[0]
            raise ValueError(
                f"enabled {self.provider} import source is missing endpoint metadata: "
                f"{', '.join(sorted(preferred - endpoint_keys))}"
            )
        if self.provider == "azure_devops" and not self.install_url:
            return self
        if not self.install_url:
            raise ValueError(f"enabled {self.provider} import source requires install_url")
        return self


class ProjectImportStartResponse(BaseModel):
    project_id: UUID
    workspace_id: UUID
    status: str
    sources: list[ProjectImportSourceOption]


class ProjectImportFinalize(BaseModel):
    """Finalize GitHub import and enqueue managed repo setup."""

    project_id: UUID
    workspace_id: UUID
    name: str
    type: ProjectType
    description: Optional[str] = None
    installation_id: Optional[int] = Field(default=None, gt=0)
    workspace_user_github_installation_id: Optional[UUID] = None
    workspace_user_gitlab_connection_id: Optional[UUID] = None
    workspace_user_bitbucket_connection_id: Optional[UUID] = None
    workspace_user_azure_connection_id: Optional[UUID] = None
    repo_full_name: str = Field(..., min_length=3, max_length=255)
    branch: Optional[str] = Field(default=None, max_length=100)
    root_dir: str = Field(default="/", max_length=255)
    import_env: bool = True

    @field_validator("repo_full_name")
    @classmethod
    def _repo_full_name(cls, value: str) -> str:
        return ProjectGithubImportSource._repo_full_name(value)

    @field_validator("root_dir")
    @classmethod
    def _root_dir(cls, value: str) -> str:
        return ProjectGithubImportSource._root_dir(value)


class ProjectUpdate(BaseModel):
    """Update project."""

    name: Optional[str] = None
    description: Optional[str] = None


class FormPolicyUpdate(BaseModel):
    """Project-owned restrictions for standardized country/address/phone form controls."""

    country_mode: Literal["all", "include", "exclude"] = "all"
    countries: list[str] = Field(default_factory=list, max_length=249)
    default_country: Optional[str] = Field(default=None, min_length=2, max_length=2)
    require_subdivision: bool = False
    phone_country_selector: bool = True


class FormPolicyResponse(FormPolicyUpdate):
    """Normalized form policy returned to the dashboard and injected into codegen context."""


class ProjectRepoMetadata(BaseModel):
    """GitHub linkage returned on project detail (ergonomic read)."""

    repo_full_name: str
    clone_url: Optional[str] = None
    html_url: Optional[str] = None
    default_branch: str = "main"
    github_repo_id: Optional[int] = None
    setup_stage: Optional[str] = None
    initial_commit_sha: Optional[str] = None


class ProjectResponse(BaseModel):
    """Project response."""

    id: UUID
    workspace_id: UUID
    slug: str
    github_repo_name: Optional[str] = None
    name: str
    description: Optional[str]
    type: str
    status: str
    created_by: Optional[UUID]
    created_at: datetime
    repo: Optional[ProjectRepoMetadata] = None
    # Deterministic from project_ref — available immediately on creation, before CF Pages builds.
    live_url: Optional[str] = None
    preview_url: Optional[str] = None
    boilerplate_version: Optional[str] = None
    template_id: Optional[UUID] = None
    template_ref: Optional[str] = None
    source_kind: Optional[str] = None
    source_workspace_user_gitlab_connection_id: Optional[UUID] = None
    source_workspace_user_bitbucket_connection_id: Optional[UUID] = None
    source_workspace_user_azure_connection_id: Optional[UUID] = None
    source_repo_full_name: Optional[str] = None
    source_branch: Optional[str] = None
    source_root_dir: Optional[str] = None
    source_import_env: bool = True

    model_config = {"from_attributes": True}


def project_to_response(project: object) -> ProjectResponse:
    """Map ORM project (optionally with ``.repo`` from get_with_repo) to API model."""
    # The managed repository is an internal implementation detail. Do not
    # expose its name, HTML URL, clone URL, or provider identifiers in the
    # project API; the frontend uses canonical P2HS URLs for previews/deploys.
    repo = None

    live_url: Optional[str] = None
    preview_url: Optional[str] = None
    project_ref = getattr(project, "project_ref", None)
    if project_ref:
        try:
            from src.config import get_settings
            from src.domain.hosting.hostnames import canonical_live_url, canonical_preview_url
            apex = get_settings().hosting_public_apex or "sites.p2hs.com"
            live_url = canonical_live_url(project_ref, apex)
            preview_url = canonical_preview_url(project_ref, apex)
        except Exception:
            pass

    return ProjectResponse(
        id=project.id,
        workspace_id=project.workspace_id,
        slug=project.slug,
        github_repo_name=None,
        name=project.name,
        description=project.description,
        type=project.type,
        status=project.status,
        created_by=project.created_by,
        created_at=project.created_at,
        repo=repo,
        live_url=live_url,
        preview_url=preview_url,
        boilerplate_version=getattr(project, "boilerplate_version", None),
        template_id=getattr(project, "template_id", None),
        template_ref=getattr(project, "template_ref", None),
        source_kind=getattr(project, "source_kind", None),
        source_workspace_user_gitlab_connection_id=getattr(
            project,
            "source_workspace_user_gitlab_connection_id",
            None,
        ),
        source_workspace_user_bitbucket_connection_id=getattr(
            project,
            "source_workspace_user_bitbucket_connection_id",
            None,
        ),
        source_workspace_user_azure_connection_id=getattr(
            project,
            "source_workspace_user_azure_connection_id",
            None,
        ),
        source_repo_full_name=getattr(project, "source_repo_full_name", None),
        source_branch=getattr(project, "source_branch", None),
        source_root_dir=getattr(project, "source_root_dir", None),
        source_import_env=bool(getattr(project, "source_import_env", True)),
    )


class EnvVarCreate(BaseModel):
    """Create env var. Values are encrypted at rest; never returned by the API."""

    key: str
    value: str
    environment: str = Field(
        default="all",
        description="Host targeting: `all` (every deploy), `production`, or `preview`.",
    )
    visibility: str = Field(
        default="secret",
        description="`secret` → CF Worker Secret binding, never in repo. `public` → plain CF env var, written to .env.example.",
    )


class EnvVarUpdate(BaseModel):
    """Rotate an env var value (and optionally change visibility). Key and environment are immutable."""

    value: str
    visibility: Optional[str] = Field(
        default=None,
        description="Change visibility. Omit to keep existing value.",
    )


class WebhookSetupComplete(BaseModel):
    """Supplies the provider-generated signing secret for a feature-pack webhook that install
    left blocked (Stripe et al. only hand back a signing secret after the dashboard endpoint is
    created using the `webhook_url` from the pack's install response)."""

    signing_secret: str
    signing_header: Optional[str] = Field(
        default=None,
        description="Header carrying the signature, for hmac_sha256 providers. Omit for Stripe.",
    )


class EnvVarResponse(BaseModel):
    """Env var metadata (key only — values are never returned)."""

    id: UUID
    key: str
    environment: str
    visibility: str
    created_at: datetime
