# #region ProfileUtils [C:2] [TYPE Module] [SEMANTICS string,normalization,utility,sanitize]
# @BRIEF Pure utility helpers for profile data sanitization, normalization, and secret masking.
#        Also contains shared exception classes to avoid circular imports between profile sub-modules.
# @LAYER Domain
# @RATIONALE Extracted from ProfileService to satisfy INV_7 — module length under 400 lines,
#            contract length under 150 lines. These are stateless pure functions that do not
#            require a class or service container.
# @REJECTED Keeping these as private methods on ProfileService was rejected because they
#            inflate the class beyond 150 lines and mix pure computation with I/O-bound
#            service logic.

from collections.abc import Iterable, Sequence
from datetime import UTC, datetime
from typing import Any


# #region ProfileValidationError [C:2] [TYPE Class]
# @RELATION INHERITS -> [EXT:Python:Exception]
# @BRIEF Domain validation error for profile preference update requests.
class ProfileValidationError(Exception):
    def __init__(self, errors: Sequence[str]):
        self.errors = list(errors)
        super().__init__("Profile preference validation failed")
# #endregion ProfileValidationError


# #region EnvironmentNotFoundError [C:2] [TYPE Class]
# @RELATION INHERITS -> [EXT:Python:Exception]
# @BRIEF Raised when environment_id from lookup request is unknown in app configuration.
class EnvironmentNotFoundError(Exception):
    pass
# #endregion EnvironmentNotFoundError


# #region ProfileAuthorizationError [C:2] [TYPE Class]
# @RELATION INHERITS -> [EXT:Python:Exception]
# @BRIEF Raised when caller attempts cross-user preference mutation.
class ProfileAuthorizationError(Exception):
    pass
# #endregion ProfileAuthorizationError

SUPPORTED_START_PAGES: set[str] = {"dashboards", "datasets", "reports"}
SUPPORTED_DENSITIES: set[str] = {"compact", "comfortable"}


# #region sanitize_text [C:1] [TYPE Function]
# @BRIEF Normalize optional text into trimmed form or None.
def sanitize_text(value: str | None) -> str | None:
    normalized = str(value or "").strip()
    if not normalized:
        return None
    return normalized
# #endregion sanitize_text


# #region sanitize_secret [C:1] [TYPE Function]
# @BRIEF Normalize secret input into trimmed form or None.
def sanitize_secret(value: str | None) -> str | None:
    if value is None:
        return None
    normalized = str(value).strip()
    if not normalized:
        return None
    return normalized
# #endregion sanitize_secret


# #region sanitize_username [C:1] [TYPE Function]
# @BRIEF Normalize raw username into trimmed form or None for empty input.
def sanitize_username(value: str | None) -> str | None:
    return sanitize_text(value)
# #endregion sanitize_username


# #region normalize_username [C:1] [TYPE Function]
# @BRIEF Apply deterministic trim+lower normalization for actor matching.
def normalize_username(value: str | None) -> str | None:
    sanitized = sanitize_username(value)
    if sanitized is None:
        return None
    return sanitized.lower()
# #endregion normalize_username


# #region normalize_start_page [C:1] [TYPE Function]
# @BRIEF Normalize supported start page aliases to canonical values.
def normalize_start_page(value: str | None) -> str:
    normalized = str(value or "").strip().lower()
    if normalized == "reports-logs":
        return "reports"
    if normalized in SUPPORTED_START_PAGES:
        return normalized
    return "dashboards"
# #endregion normalize_start_page


# #region normalize_density [C:1] [TYPE Function]
# @BRIEF Normalize supported density aliases to canonical values.
def normalize_density(value: str | None) -> str:
    normalized = str(value or "").strip().lower()
    if normalized == "free":
        return "comfortable"
    if normalized in SUPPORTED_DENSITIES:
        return normalized
    return "comfortable"
# #endregion normalize_density


# #region mask_secret_value [C:1] [TYPE Function]
# @BRIEF Build a safe display value for sensitive secrets.
def mask_secret_value(secret: str | None) -> str | None:
    sanitized = sanitize_secret(secret)
    if sanitized is None:
        return None
    if len(sanitized) <= 4:
        return "***"
    return f"{sanitized[:2]}***{sanitized[-2:]}"
# #endregion mask_secret_value


# #region validate_update_payload [C:2] [TYPE Function]
# @BRIEF Validate username/toggle constraints for preference mutation.
# @RELATION CALLS -> [sanitize_username]
# @RELATION CALLS -> [sanitize_text]
# @RELATION DEPENDS_ON -> [ProfileUtils]
# @RELATION DEPENDS_ON -> [ProfileUtils]
# @POST Returns validation errors list; empty list means valid.
def validate_update_payload(
    superset_username: str | None,
    show_only_my_dashboards: bool,
    git_email: str | None,
    start_page: str,
    dashboards_table_density: str,
    email_address: str | None = None,
) -> list[str]:
    errors: list[str] = []
    sanitized_username = sanitize_username(superset_username)

    if sanitized_username and any(ch.isspace() for ch in sanitized_username):
        errors.append(
            "Username should not contain spaces. Please enter a valid Apache Superset username."
        )
    if show_only_my_dashboards and not sanitized_username:
        errors.append(
            "Superset username is required when default filter is enabled."
        )

    sanitized_git_email = sanitize_text(git_email)
    if sanitized_git_email:
        if (
            " " in sanitized_git_email
            or "@" not in sanitized_git_email
            or sanitized_git_email.startswith("@")
            or sanitized_git_email.endswith("@")
        ):
            errors.append("Git email should be a valid email address.")

    if start_page not in SUPPORTED_START_PAGES:
        errors.append("Start page value is not supported.")

    if dashboards_table_density not in SUPPORTED_DENSITIES:
        errors.append("Dashboards table density value is not supported.")

    sanitized_email = sanitize_text(email_address)
    if sanitized_email:
        if (
            " " in sanitized_email
            or "@" not in sanitized_email
            or sanitized_email.startswith("@")
            or sanitized_email.endswith("@")
        ):
            errors.append("Notification email should be a valid email address.")

    return errors
# #endregion validate_update_payload


# #region build_default_preference [C:2] [TYPE Function] [SEMANTICS preference,default,unconfigured]
# @BRIEF Build non-persisted default preference DTO for unconfigured users.
# @RELATION DEPENDS_ON -> [ProfilePreference]
def build_default_preference(user_id: str) -> Any:
    """Build default ProfilePreference with disabled toggles and empty usernames."""
    from ..schemas.profile import ProfilePreference
    now = datetime.now(UTC)
    return ProfilePreference(
        user_id=str(user_id),
        superset_username=None,
        superset_username_normalized=None,
        show_only_my_dashboards=False,
        show_only_slug_dashboards=True,
        git_username=None,
        git_email=None,
        has_git_personal_access_token=False,
        git_personal_access_token_masked=None,
        start_page="dashboards",
        auto_open_task_drawer=True,
        dashboards_table_density="comfortable",
        telegram_id=None,
        email_address=None,
        notify_on_fail=True,
        created_at=now,
        updated_at=now,
    )
# #endregion build_default_preference


# #region normalize_owner_tokens [C:2] [TYPE Function]
# @BRIEF Normalize owners payload into deduplicated lower-cased tokens.
# @RELATION CALLS -> [normalize_username]
def normalize_owner_tokens(owners: Iterable[Any] | None) -> list[str]:
    if owners is None:
        return []
    normalized: list[str] = []
    for owner in owners:
        owner_candidates: list[Any]
        if isinstance(owner, dict):
            first_name = sanitize_username(str(owner.get("first_name") or ""))
            last_name = sanitize_username(str(owner.get("last_name") or ""))
            full_name = " ".join(
                part for part in [first_name, last_name] if part
            ).strip()
            snake_name = "_".join(
                part for part in [first_name, last_name] if part
            ).strip("_")
            owner_candidates = [
                owner.get("username"),
                owner.get("user_name"),
                owner.get("name"),
                owner.get("full_name"),
                first_name,
                last_name,
                full_name or None,
                snake_name or None,
                owner.get("email"),
            ]
        else:
            owner_candidates = [owner]

        for candidate in owner_candidates:
            token = normalize_username(str(candidate or ""))
            if token and token not in normalized:
                normalized.append(token)
    return normalized
# #endregion normalize_owner_tokens
# #endregion ProfileUtils
