from datetime import datetime
from enum import Enum

from pydantic import AliasChoices, BaseModel, EmailStr, Field, field_validator

from app.google_drive_link import GOOGLE_DRIVE_LINK_ERROR, validate_google_drive_folder_link


class RoleEnum(str, Enum):
    admin = "admin"
    user = "user"


# --- Auth ---
class Token(BaseModel):
    access_token: str
    token_type: str = "bearer"


class UserTokenResponse(BaseModel):
    """User login response with verification state."""

    access_token: str
    token_type: str = "bearer"
    email: EmailStr
    email_verified: bool


class TokenPayload(BaseModel):
    sub: int
    role: RoleEnum
    exp: int | None = None


class LoginRequest(BaseModel):
    email: EmailStr
    password: str


class ForgotPasswordRequest(BaseModel):
    email: EmailStr


class VerifyEmailRequest(BaseModel):
    email: EmailStr
    otp: str = Field(..., min_length=4, max_length=10)


class VerifyOTPRequest(BaseModel):
    email: EmailStr
    otp: str = Field(..., min_length=4, max_length=10)


class ResetPasswordRequest(BaseModel):
    model_config = {"populate_by_name": True}

    email: EmailStr
    otp: str = Field(
        ...,
        min_length=4,
        max_length=10,
        validation_alias=AliasChoices("otp", "code", "verification_code", "verificationCode"),
    )
    new_password: str = Field(
        ...,
        min_length=8,
        max_length=128,
        validation_alias=AliasChoices("new_password", "newPassword", "password"),
    )

    @field_validator("new_password")
    @classmethod
    def _strong_reset_password(cls, v: str) -> str:
        v = v.strip()
        if " " in v:
            raise ValueError("Password cannot contain spaces")
        import re

        if not re.search(r"[a-z]", v):
            raise ValueError("Password must include at least one lowercase letter")
        if not re.search(r"[A-Z]", v):
            raise ValueError("Password must include at least one uppercase letter")
        if not re.search(r"\d", v):
            raise ValueError("Password must include at least one number")
        if not re.search(r"[^A-Za-z0-9]", v):
            raise ValueError("Password must include at least one special character")
        return v


class ChangePasswordRequest(BaseModel):
    """Logged-in user changes password (current + new + confirm)."""

    model_config = {"populate_by_name": True}

    current_password: str = Field(
        ...,
        min_length=1,
        validation_alias=AliasChoices(
            "current_password",
            "currentPassword",
            "password",
            "old_password",
            "oldPassword",
        ),
    )
    new_password: str = Field(
        ...,
        min_length=8,
        max_length=128,
        validation_alias=AliasChoices("new_password", "newPassword"),
    )
    confirm_password: str = Field(
        ...,
        min_length=8,
        max_length=128,
        validation_alias=AliasChoices(
            "confirm_password",
            "confirmPassword",
            "password_confirmation",
            "passwordConfirmation",
        ),
    )

    @field_validator("new_password")
    @classmethod
    def _strong_new_password(cls, v: str) -> str:
        v = v.strip()
        if " " in v:
            raise ValueError("Password cannot contain spaces")
        import re

        if not re.search(r"[a-z]", v):
            raise ValueError("Password must include at least one lowercase letter")
        if not re.search(r"[A-Z]", v):
            raise ValueError("Password must include at least one uppercase letter")
        if not re.search(r"\d", v):
            raise ValueError("Password must include at least one number")
        if not re.search(r"[^A-Za-z0-9]", v):
            raise ValueError("Password must include at least one special character")
        return v

    @field_validator("confirm_password")
    @classmethod
    def passwords_match(cls, v: str, info) -> str:
        new_password = info.data.get("new_password")
        if new_password is not None and v != new_password:
            raise ValueError("New password and confirm password do not match")
        return v


# --- User / Profile ---
class UserBase(BaseModel):
    email: EmailStr
    full_name: str = ""
    is_active: bool = True


class UserCreate(UserBase):
    password: str = Field(..., min_length=8)
    role: RoleEnum = RoleEnum.user


class UserUpdate(BaseModel):
    email: EmailStr | None = None
    full_name: str | None = None
    is_active: bool | None = None
    password: str | None = Field(None, min_length=8)
    role: RoleEnum | None = None


class UserIdRequest(BaseModel):
    user_id: int = Field(..., ge=1, description="Target user primary key")


class UpdateUserRequest(UserUpdate):
    user_id: int = Field(..., ge=1, description="User to update")


class UserProgrammeProgressPublic(BaseModel):
    level_slug: str | None = None
    level_display_name: str | None = None
    module_kind: str | None = None
    module_display_name: str | None = None
    week_index: int | None = None
    week_label: str | None = None
    session_number: int | None = None
    session_title: str | None = None
    prior_completed_assessment_count: int | None = None
    prior_completed_session_markers_count: int | None = None


class UserPublic(BaseModel):
    id: int
    email: EmailStr
    full_name: str
    phone_number: str | None = None
    address: str | None = None
    postal_code: str | None = None
    profile_image: str | None = None
    profile_image_url: str | None = None
    has_profile_image: bool = False
    role: RoleEnum
    is_active: bool
    email_verified: bool = False
    created_at: datetime
    programme: UserProgrammeProgressPublic | None = None
    total_points: int | None = None
    assigned_points_total: int | None = None
    available_points: int | None = None
    progress_percent: int | None = None
    google_drive_link: str | None = None

    model_config = {"from_attributes": True}


class AdminUsersListResponse(BaseModel):
    total: int
    users: list[UserPublic]


class AdminMemberPersonalInfoUpdateRequest(BaseModel):
    model_config = {"populate_by_name": True}

    user_id: int = Field(..., ge=1)
    full_name: str | None = None
    programme_level_slug: str | None = Field(
        default=None,
        validation_alias=AliasChoices("programme_level_slug", "level_slug", "level"),
    )
    programme_module_kind: str | None = Field(
        default=None,
        validation_alias=AliasChoices("programme_module_kind", "module_kind", "module"),
    )
    programme_week_index: int | None = Field(
        default=None,
        ge=0,
        le=12,
        validation_alias=AliasChoices("programme_week_index", "week_index", "week"),
    )
    programme_session_number: int | None = Field(
        default=None,
        ge=1,
        validation_alias=AliasChoices("programme_session_number", "session_number", "session"),
    )
    google_drive_link: str | None = Field(
        default=None,
        validation_alias=AliasChoices("google_drive_link", "googleDriveLink", "drive_link"),
    )
    phone_number: str | None = Field(
        default=None,
        max_length=32,
        validation_alias=AliasChoices("phone_number", "phoneNumber", "phone"),
    )

    @field_validator("google_drive_link", mode="before")
    @classmethod
    def validate_google_drive_link(cls, value: str | None) -> str | None:
        if value is None or (isinstance(value, str) and not value.strip()):
            return None
        try:
            return validate_google_drive_folder_link(str(value))
        except ValueError as exc:
            raise ValueError(GOOGLE_DRIVE_LINK_ERROR) from exc


class AdminMemberGoogleDriveUpdateRequest(BaseModel):
    """Attach or update a member's personalized Google Drive folder for WhatsApp video submissions."""

    model_config = {"populate_by_name": True}

    user_id: int = Field(..., ge=1)
    google_drive_link: str | None = Field(
        default=None,
        max_length=512,
        validation_alias=AliasChoices("google_drive_link", "googleDriveLink", "drive_link"),
    )

    @field_validator("google_drive_link", mode="before")
    @classmethod
    def validate_google_drive_link(cls, value: str | None) -> str | None:
        if value is None or (isinstance(value, str) and not value.strip()):
            return None
        try:
            return validate_google_drive_folder_link(str(value))
        except ValueError as exc:
            raise ValueError(GOOGLE_DRIVE_LINK_ERROR) from exc


class MemberGoogleDriveEntryOut(BaseModel):
    id: int
    full_name: str
    email: str
    phone_number: str | None = None
    google_drive_link: str | None = None
    has_google_drive_link: bool = False
    week_label: str | None = None
    level_display_name: str | None = None
    module_display_name: str | None = None


class MemberGoogleDriveListResponse(BaseModel):
    total: int
    members: list[MemberGoogleDriveEntryOut]


class MemberInactiveFollowUpOut(BaseModel):
    id: int
    user_id: int
    full_name: str
    email: str
    last_active_at: datetime | None = None
    days_inactive: int
    follow_up_sent: bool = False
    follow_up_sent_at: datetime | None = None
    member: UserPublic | None = None


class MemberInactiveFollowUpListResponse(BaseModel):
    total: int
    inactive_days: int = 10
    members: list[MemberInactiveFollowUpOut]


class MemberInactiveFollowUpUserIdRequest(BaseModel):
    user_id: int = Field(..., ge=1)


class AdminMemberAccountUpdateRequest(BaseModel):
    """Admin-only account controls for member login credentials."""

    model_config = {"populate_by_name": True}

    user_id: int = Field(..., ge=1)
    email: EmailStr | None = None
    password: str | None = Field(default=None, min_length=8)


class UpdateUserProgrammeRequest(BaseModel):
    model_config = {"populate_by_name": True}

    user_id: int = Field(..., ge=1)
    programme_level_slug: str = Field(
        validation_alias=AliasChoices("programme_level_slug", "level_slug", "level"),
    )
    programme_module_kind: str = Field(
        validation_alias=AliasChoices("programme_module_kind", "module_kind", "module"),
    )
    programme_week_index: int = Field(
        default=0,
        ge=0,
        le=12,
        validation_alias=AliasChoices("programme_week_index", "week_index", "week"),
    )
    programme_session_number: int = Field(
        default=1,
        ge=1,
        validation_alias=AliasChoices("programme_session_number", "session_number", "session"),
    )


class MemberWeekSessionDetailOut(BaseModel):
    session_id: int
    session_number: int
    title: str
    status: str
    video_submitted: bool
    is_completed: bool
    has_feedback: bool
    feedback: str | None = None
    assigned_points: int = 0
    progress_id: int | None = None
    image_url: str | None = None
    image_urls: list[str] = Field(default_factory=list)
    first_assessment_id: int | None = None
    first_assessment_title: str | None = None


class MemberWeekDetailOut(BaseModel):
    week_id: int
    week_index: int
    week_number: int
    display_label: str
    total_sessions: int
    completed_sessions: int
    is_completed: bool
    week_point_awarded: bool = False
    expanded: bool = False
    sessions: list[MemberWeekSessionDetailOut]


class MemberWeeksDetailResponse(BaseModel):
    user_id: int
    level_slug: str | None = None
    level_display_name: str | None = None
    module_kind: str | None = None
    module_display_name: str | None = None
    current_week_index: int | None = None
    assigned_points_total: int | None = None
    available_points: int | None = None
    weeks: list[MemberWeekDetailOut]


class MarkMemberWeekCompleteRequest(BaseModel):
    model_config = {"populate_by_name": True}

    user_id: int = Field(..., ge=1)
    week_index: int = Field(..., ge=0, le=12)
    is_completed: bool = Field(default=True)


class UserRegisterRequest(BaseModel):
    email: EmailStr
    password: str = Field(..., min_length=8)
    full_name: str = Field(
        default="",
        validation_alias=AliasChoices("full_name", "name"),
    )

    model_config = {"populate_by_name": True}


class UserRegisterStepSaveRequest(BaseModel):
    """Draft selections only — cannot set current_step or is_completed (server controls those)."""

    model_config = {"populate_by_name": True}

    level_id: int | None = Field(default=None, ge=1, validation_alias=AliasChoices("level_id", "levelId"))
    level_name: str | None = Field(default=None, validation_alias=AliasChoices("level_name", "levelName"))
    module_id: int | None = Field(default=None, ge=1, validation_alias=AliasChoices("module_id", "moduleId"))
    module_name: str | None = Field(default=None, validation_alias=AliasChoices("module_name", "moduleName"))
    week_id: int | None = Field(default=None, ge=1, validation_alias=AliasChoices("week_id", "weekId"))
    week_name: str | None = Field(default=None, validation_alias=AliasChoices("week_name", "weekName"))
    session_id: int | None = Field(default=None, ge=1, validation_alias=AliasChoices("session_id", "sessionId"))
    session_name: str | None = Field(default=None, validation_alias=AliasChoices("session_name", "sessionName"))


class UserRegisterStepOut(BaseModel):
    id: int
    user_id: int
    level_id: int | None = None
    level_name: str | None = None
    module_id: int | None = None
    module_name: str | None = None
    week_id: int | None = None
    week_name: str | None = None
    session_id: int | None = None
    session_name: str | None = None
    current_step: str
    is_completed: bool
    created_at: datetime
    updated_at: datetime

    model_config = {"from_attributes": True}


class RegisterStepStatus(str, Enum):
    locked = "locked"
    open = "open"
    completed = "completed"


class RegisterStepItemOut(BaseModel):
    """One onboarding step (level → module → week → session) and whether the user can use it."""

    step: str
    label: str
    status: RegisterStepStatus
    selected_id: int | None = None
    selected_name: str | None = None


class UserRegisterStepStateResponse(BaseModel):
    """Frontend-friendly register-step state: which step is open, locked, or done."""

    current_step: str
    is_completed: bool
    can_proceed: bool
    steps: list[RegisterStepItemOut]
    selection: UserRegisterStepOut | None = None


class UserRegisterStepAcceptRequest(BaseModel):
    """Save the user's choice for the current open step and move to the next step."""

    model_config = {"populate_by_name": True}

    level_id: int | None = Field(default=None, ge=1, validation_alias=AliasChoices("level_id", "levelId"))
    level_name: str | None = Field(default=None, validation_alias=AliasChoices("level_name", "levelName"))
    module_id: int | None = Field(default=None, ge=1, validation_alias=AliasChoices("module_id", "moduleId"))
    module_name: str | None = Field(default=None, validation_alias=AliasChoices("module_name", "moduleName"))
    week_id: int | None = Field(default=None, ge=1, validation_alias=AliasChoices("week_id", "weekId"))
    week_name: str | None = Field(default=None, validation_alias=AliasChoices("week_name", "weekName"))
    session_id: int | None = Field(default=None, ge=1, validation_alias=AliasChoices("session_id", "sessionId"))
    session_name: str | None = Field(default=None, validation_alias=AliasChoices("session_name", "sessionName"))


class TrackProgressStatus(str, Enum):
    locked = "locked"
    open = "open"
    completed = "completed"


class TrackAssessmentOut(BaseModel):
    id: int
    sort_order: int
    title: str
    status: TrackProgressStatus


class TrackSessionOut(BaseModel):
    id: int
    session_number: int
    title: str
    admin_checked: bool
    status: TrackProgressStatus
    assessments_count: int
    assessments: list[TrackAssessmentOut]


class TrackWeekOut(BaseModel):
    id: int
    week_index: int
    display_label: str
    status: TrackProgressStatus
    sessions_count: int
    completed_sessions_count: int
    sessions: list[TrackSessionOut]


class TrackModuleOut(BaseModel):
    id: int
    kind: str
    display_name: str
    status: TrackProgressStatus
    weeks: list[TrackWeekOut]
    completed_sessions: int
    total_sessions: int


class TrackLevelOut(BaseModel):
    id: int
    slug: str
    display_name: str
    modules: list[TrackModuleOut]
    completed_sessions: int
    total_sessions: int


class ProgrammeTrackResponse(BaseModel):
    registration_completed: bool
    level_unlocked: bool = False
    module_unlocked: bool = True
    current_open: dict | None = None
    registration_selection: dict | None = None
    level: TrackLevelOut | None = None


class TrackCompleteRequest(BaseModel):
    model_config = {"populate_by_name": True}

    assessment_id: int | None = Field(default=None, ge=1, validation_alias=AliasChoices("assessment_id", "assessmentId"))
    session_id: int | None = Field(default=None, ge=1, validation_alias=AliasChoices("session_id", "sessionId"))
    level: str | None = Field(default=None, description="Optional level slug filter, e.g. beginner")
    module_kind: str | None = Field(default=None, validation_alias=AliasChoices("module_kind", "moduleKind"))


class UserTrackTraceOut(BaseModel):
    id: int
    user_id: int
    level_id: int | None = None
    level_slug: str | None = None
    level_name: str | None = None
    module_id: int | None = None
    module_kind: str | None = None
    module_name: str | None = None
    week_id: int | None = None
    week_name: str | None = None
    week_index: int | None = None
    session_id: int | None = None
    session_name: str | None = None
    session_number: int | None = None
    assessment_id: int | None = None
    assessment_title: str | None = None
    action: str
    session_fully_completed: bool
    created_at: datetime

    model_config = {"from_attributes": True}


class UserTrackTraceListResponse(BaseModel):
    total: int
    traces: list[UserTrackTraceOut]


class ProgrammeAssessmentOut(BaseModel):
    id: int
    sort_order: int
    title: str


class ProgrammeSessionOut(BaseModel):
    id: int
    session_number: int
    title: str
    admin_checked: bool
    assessments_count: int
    assessments: list[ProgrammeAssessmentOut]


class ProgrammeWeekOut(BaseModel):
    id: int
    week_index: int
    display_label: str
    sessions_count: int
    assessments_count: int
    sessions: list[ProgrammeSessionOut]


# --- Member points ---
class MemberPointsOut(BaseModel):
    user_id: int
    assigned_points_total: int
    available_points: int

    model_config = {"from_attributes": True}


class ProgrammeModuleOut(BaseModel):
    id: int
    kind: str
    display_name: str
    sort_order: int
    weeks_count: int
    sessions_count: int
    assessments_count: int
    weeks: list[ProgrammeWeekOut]


class ProgrammeLevelOut(BaseModel):
    id: int
    slug: str
    display_name: str
    sort_order: int
    modules_count: int
    weeks_count: int
    sessions_count: int
    assessments_count: int
    modules: list[ProgrammeModuleOut]


class ProgrammeCatalogResponse(BaseModel):
    total_levels: int
    total_modules: int
    total_weeks: int
    total_sessions: int
    total_assessments: int
    levels: list[ProgrammeLevelOut]


# --- Programme CRUD (admin) ---
class ProgrammeLevelCreateRequest(BaseModel):
    model_config = {"populate_by_name": True}

    slug: str = Field(..., min_length=1, max_length=32)
    display_name: str = Field(
        ...,
        min_length=1,
        max_length=255,
        validation_alias=AliasChoices("display_name", "displayName", "name"),
    )
    sort_order: int = Field(
        default=0,
        ge=0,
        validation_alias=AliasChoices("sort_order", "sortOrder", "sort"),
    )


class ProgrammeAssessmentTreeCreateRequest(BaseModel):
    model_config = {"populate_by_name": True}

    title: str = Field(..., min_length=1, max_length=512, validation_alias=AliasChoices("title", "name"))
    sort_order: int = Field(
        default=0,
        ge=0,
        validation_alias=AliasChoices("sort_order", "sortOrder", "sort"),
    )


class ProgrammeSessionTreeCreateRequest(BaseModel):
    model_config = {"populate_by_name": True}

    session_number: int = Field(
        ...,
        ge=1,
        validation_alias=AliasChoices("session_number", "sessionNumber", "number"),
    )
    title: str = Field(..., min_length=1, max_length=512, validation_alias=AliasChoices("title", "name"))
    admin_checked: bool = Field(default=False, validation_alias=AliasChoices("admin_checked", "adminChecked"))
    assessments: list[ProgrammeAssessmentTreeCreateRequest] = Field(default_factory=list)


class ProgrammeWeekTreeCreateRequest(BaseModel):
    model_config = {"populate_by_name": True}

    week_index: int = Field(
        ...,
        ge=0,
        le=12,
        validation_alias=AliasChoices("week_index", "weekIndex", "index"),
    )
    display_label: str = Field(
        ...,
        min_length=1,
        max_length=64,
        validation_alias=AliasChoices("display_label", "displayLabel", "label", "name"),
    )
    sessions: list[ProgrammeSessionTreeCreateRequest] = Field(default_factory=list)


class ProgrammeModuleTreeCreateRequest(BaseModel):
    model_config = {"populate_by_name": True}

    kind: str = Field(..., min_length=1, max_length=64)
    display_name: str = Field(
        ...,
        min_length=1,
        max_length=255,
        validation_alias=AliasChoices("display_name", "displayName", "name"),
    )
    sort_order: int = Field(
        default=0,
        ge=0,
        validation_alias=AliasChoices("sort_order", "sortOrder", "sort"),
    )
    weeks: list[ProgrammeWeekTreeCreateRequest] = Field(default_factory=list)


class ProgrammeLevelTreeCreateRequest(BaseModel):
    """
    Payload for `/admin/programme/levels/full` (one request creates full tree).
    """

    model_config = {"populate_by_name": True}

    slug: str = Field(..., min_length=1, max_length=32)
    display_name: str = Field(
        ...,
        min_length=1,
        max_length=255,
        validation_alias=AliasChoices("display_name", "displayName", "name"),
    )
    sort_order: int = Field(
        default=0,
        ge=0,
        validation_alias=AliasChoices("sort_order", "sortOrder", "sort"),
    )
    modules: list[ProgrammeModuleTreeCreateRequest] = Field(default_factory=list)


class ProgrammeLevelUpdateRequest(BaseModel):
    model_config = {"populate_by_name": True}

    level_id: int = Field(
        ...,
        ge=1,
        validation_alias=AliasChoices("level_id", "levelId", "id"),
    )
    display_name: str | None = Field(
        default=None,
        max_length=255,
        validation_alias=AliasChoices("display_name", "displayName", "name"),
    )
    sort_order: int | None = Field(
        default=None,
        ge=0,
        validation_alias=AliasChoices("sort_order", "sortOrder", "sort"),
    )


class ProgrammeModuleCreateRequest(BaseModel):
    model_config = {"populate_by_name": True}

    level_id: int = Field(
        ...,
        ge=1,
        validation_alias=AliasChoices("level_id", "levelId"),
    )
    kind: str = Field(..., min_length=1, max_length=64)
    display_name: str = Field(
        ...,
        min_length=1,
        max_length=255,
        validation_alias=AliasChoices("display_name", "displayName", "name"),
    )
    sort_order: int = Field(
        default=0,
        ge=0,
        validation_alias=AliasChoices("sort_order", "sortOrder", "sort"),
    )


class ProgrammeModuleUpdateRequest(BaseModel):
    model_config = {"populate_by_name": True}

    module_id: int = Field(
        ...,
        ge=1,
        validation_alias=AliasChoices("module_id", "moduleId", "id"),
    )
    display_name: str | None = Field(
        default=None,
        max_length=255,
        validation_alias=AliasChoices("display_name", "displayName", "name"),
    )
    sort_order: int | None = Field(
        default=None,
        ge=0,
        validation_alias=AliasChoices("sort_order", "sortOrder", "sort"),
    )


class ProgrammeWeekCreateRequest(BaseModel):
    model_config = {"populate_by_name": True}

    module_id: int = Field(
        ...,
        ge=1,
        validation_alias=AliasChoices("module_id", "moduleId"),
    )
    week_index: int = Field(..., ge=0, le=12)
    display_label: str = Field(
        ...,
        min_length=1,
        max_length=64,
        validation_alias=AliasChoices("display_label", "displayLabel", "label", "name"),
    )


class ProgrammeWeekUpdateRequest(BaseModel):
    model_config = {"populate_by_name": True}

    week_id: int = Field(
        ...,
        ge=1,
        validation_alias=AliasChoices("week_id", "weekId", "id"),
    )
    display_label: str | None = Field(
        default=None,
        max_length=64,
        validation_alias=AliasChoices("display_label", "displayLabel", "label", "name"),
    )
    week_index: int | None = Field(
        default=None,
        ge=0,
        le=12,
        validation_alias=AliasChoices("week_index", "weekIndex", "index"),
    )


class ProgrammeSessionCreateRequest(BaseModel):
    model_config = {"populate_by_name": True}

    week_id: int = Field(
        ...,
        ge=1,
        validation_alias=AliasChoices("week_id", "weekId"),
    )
    session_number: int = Field(..., ge=1)
    title: str = Field(..., min_length=1, max_length=512, validation_alias=AliasChoices("title", "name"))
    admin_checked: bool = False


class ProgrammeSessionUpdateRequest(BaseModel):
    model_config = {"populate_by_name": True}

    session_id: int = Field(
        ...,
        ge=1,
        validation_alias=AliasChoices("session_id", "sessionId", "id"),
    )
    title: str | None = Field(
        default=None,
        max_length=512,
        validation_alias=AliasChoices("title", "name"),
    )
    session_number: int | None = Field(
        default=None,
        ge=1,
        validation_alias=AliasChoices("session_number", "sessionNumber", "number"),
    )
    admin_checked: bool | None = None


class ProgrammeAssessmentCreateRequest(BaseModel):
    model_config = {"populate_by_name": True}

    session_id: int = Field(
        ...,
        ge=1,
        validation_alias=AliasChoices("session_id", "sessionId"),
    )
    title: str = Field(..., min_length=1, max_length=512, validation_alias=AliasChoices("title", "name"))
    sort_order: int = Field(
        ...,
        ge=0,
        validation_alias=AliasChoices("sort_order", "sortOrder", "sort"),
    )


class ProgrammeAssessmentUpdateRequest(BaseModel):
    model_config = {"populate_by_name": True}

    assessment_id: int = Field(
        ...,
        ge=1,
        validation_alias=AliasChoices("assessment_id", "assessmentId", "id"),
    )
    title: str | None = Field(
        default=None,
        max_length=512,
        validation_alias=AliasChoices("title", "name"),
    )
    sort_order: int | None = Field(
        default=None,
        ge=0,
        validation_alias=AliasChoices("sort_order", "sortOrder", "sort"),
    )


class ProgrammeIdRequest(BaseModel):
    """Generic single-id request for level/module/week/session/assessment delete."""
    id: int = Field(..., ge=1)


class AdminProfilePublic(BaseModel):
    """Admin profile row returned to the admin UI."""

    id: int
    user_id: int
    first_name: str
    last_name: str
    full_name: str
    phone_number: str | None = None
    email: EmailStr
    role: RoleEnum = RoleEnum.admin
    profile_image: str | None = None
    profile_image_url: str | None = None
    has_profile_image: bool = False
    created_at: datetime
    updated_at: datetime

    model_config = {"from_attributes": True}


class AdminProfileUpdate(BaseModel):
    """Admin profile edit form (name + email + phone)."""

    model_config = {"populate_by_name": True}

    full_name: str | None = Field(
        default=None,
        max_length=255,
        validation_alias=AliasChoices("full_name", "fullName", "name"),
    )
    first_name: str | None = Field(
        default=None,
        max_length=128,
        validation_alias=AliasChoices("first_name", "firstName"),
    )
    last_name: str | None = Field(
        default=None,
        max_length=128,
        validation_alias=AliasChoices("last_name", "lastName"),
    )
    phone_number: str | None = Field(
        default=None,
        max_length=32,
        validation_alias=AliasChoices("phone_number", "phoneNumber", "phone"),
    )
    email: EmailStr | None = None

    @field_validator("first_name", "last_name", "full_name", mode="after")
    @classmethod
    def _strip_names(cls, v: str | None) -> str | None:
        if v is None:
            return None
        v = v.strip()
        return v or None

    @field_validator("phone_number", mode="after")
    @classmethod
    def _validate_phone(cls, v: str | None) -> str | None:
        if v is None:
            return None
        v = v.strip()
        if not v:
            return None
        # Allow international formats with spaces, dots, dashes, parentheses.
        import re

        if not re.fullmatch(r"[0-9+()\-\.\s]{7,32}", v):
            raise ValueError("Invalid phone number format")
        return v


class ProfileUpdate(BaseModel):
    full_name: str | None = Field(
        default=None,
        max_length=255,
        validation_alias=AliasChoices("full_name", "fullName", "name"),
    )
    email: EmailStr | None = Field(default=None, max_length=255)
    phone_number: str | None = Field(
        default=None,
        max_length=32,
        validation_alias=AliasChoices("phone_number", "phoneNumber", "phone"),
    )
    address: str | None = Field(default=None, max_length=512)
    postal_code: str | None = Field(
        default=None,
        max_length=32,
        validation_alias=AliasChoices("postal_code", "postalCode", "zip", "zipcode"),
    )
    password: str | None = Field(default=None, min_length=8, max_length=128)

    model_config = {"populate_by_name": True}

    @field_validator("phone_number", mode="after")
    @classmethod
    def _validate_user_phone(cls, v: str | None) -> str | None:
        if v is None:
            return None
        v = v.strip()
        if not v:
            return None
        import re

        if not re.fullmatch(r"[0-9+()\-\.\s]{7,32}", v):
            raise ValueError("Invalid phone number format")
        return v

    @field_validator("postal_code", mode="after")
    @classmethod
    def _validate_postal(cls, v: str | None) -> str | None:
        if v is None:
            return None
        v = v.strip()
        if not v:
            return None
        import re

        if not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9\s\-]{1,31}", v):
            raise ValueError("Invalid postal code format")
        return v


class Message(BaseModel):
    message: str


class ContactMessageCreateRequest(BaseModel):
    model_config = {"populate_by_name": True}

    full_name: str = Field(
        ...,
        min_length=1,
        max_length=255,
        validation_alias=AliasChoices("full_name", "fullName", "name"),
    )
    email: EmailStr
    phone_number: str | None = Field(
        default=None,
        max_length=32,
        validation_alias=AliasChoices("phone_number", "phoneNumber", "phone"),
    )
    subject: str = Field(..., min_length=1, max_length=255)
    message: str = Field(..., min_length=1, max_length=5000)

    @field_validator("phone_number", mode="after")
    @classmethod
    def _strip_phone(cls, v: str | None) -> str | None:
        if v is None:
            return None
        v = v.strip()
        return v or None


class ContactMessageOut(BaseModel):
    id: int
    full_name: str
    email: EmailStr
    phone_number: str | None = None
    subject: str
    message: str
    email_sent: bool
    created_at: datetime

    model_config = {"from_attributes": True}


class ContactMessageListResponse(BaseModel):
    total: int
    messages: list[ContactMessageOut]


class ContactMessageSubmitResponse(BaseModel):
    message: str
    contact: ContactMessageOut


# --- Community (admin) ---
class CommunityPostCommentOut(BaseModel):
    id: int
    post_id: int
    user_id: int
    user_name: str | None = None
    comment: str
    created_at: datetime
    updated_at: datetime


class CommunityPostOut(BaseModel):
    id: int
    title: str
    post_type: str
    content: str
    media_url: str | None = None
    media_content_type: str | None = None
    has_media: bool = False
    created_by_id: int | None = None
    created_by_name: str | None = None
    created_at: datetime
    updated_at: datetime
    likes_count: int = 0
    comments_count: int = 0
    shares_count: int = 0
    share_counts: dict[str, int] = Field(default_factory=dict)
    liked_by_me: bool = False
    comments: list[CommunityPostCommentOut] = Field(default_factory=list)


class CommunityPostListResponse(BaseModel):
    total: int
    posts: list[CommunityPostOut]


class CommunityPostIdRequest(BaseModel):
    post_id: int = Field(..., ge=1, description="Community post primary key to delete")


class CommunityPostCommentRequest(BaseModel):
    post_id: int = Field(..., ge=1, description="Community post primary key")
    comment: str = Field(..., min_length=1, max_length=1000)


class CommunityPostCommentIdRequest(BaseModel):
    model_config = {"populate_by_name": True}

    comment_id: int = Field(
        ...,
        ge=1,
        description="Community comment primary key to delete",
        validation_alias=AliasChoices("comment_id", "commentId"),
    )
    post_id: int | None = Field(
        default=None,
        ge=1,
        description="Optional post id to scope the delete lookup",
        validation_alias=AliasChoices("post_id", "postId"),
    )


class CommunityPostShareRequest(BaseModel):
    post_id: int = Field(..., ge=1, description="Community post primary key")
    platform: str = Field(..., pattern="^(instagram|facebook|whatsapp)$")


# --- Rewards (admin) ---
class RewardOut(BaseModel):
    id: int
    name: str
    description: str
    points_required: int
    category: str
    media_url: str | None = None
    media_content_type: str | None = None
    has_media: bool = False
    created_by_id: int | None = None
    created_by_name: str | None = None
    created_at: datetime
    updated_at: datetime


class RewardListResponse(BaseModel):
    total: int
    rewards: list[RewardOut]
    available_points: int | None = None
    total_points: int | None = None
    assigned_points_total: int | None = None


class RewardIdRequest(BaseModel):
    reward_id: int = Field(..., ge=1, description="Reward primary key to delete")


class RewardClaimRequest(BaseModel):
    reward_id: int = Field(..., ge=1, description="Reward product to claim")
    phone_number: str | None = Field(default=None, max_length=32)
    location: str | None = Field(default=None, max_length=255)
    total_points: int | None = Field(
        default=None,
        ge=0,
        description="Optional user total points. Defaults to the reward points_required.",
    )


class RewardClaimIdRequest(BaseModel):
    claim_id: int = Field(..., ge=1, description="Reward claim primary key")


class RewardClaimStatusRequest(BaseModel):
    claim_id: int = Field(..., ge=1)
    status: str = Field(..., description="accept | accepted | reject | rejected | pending")

    @field_validator("status")
    @classmethod
    def validate_status(cls, value: str) -> str:
        normalized = value.strip().lower()
        aliases = {
            "accept": "accepted",
            "accepted": "accepted",
            "approve": "accepted",
            "approved": "accepted",
            "reject": "rejected",
            "rejected": "rejected",
            "pending": "pending",
        }
        if normalized not in aliases:
            raise ValueError("status must be one of: accepted, rejected, pending")
        return aliases[normalized]


class RewardClaimOut(BaseModel):
    id: int
    reward_id: int | None = None
    product_name: str
    user_id: int
    user_name: str
    email: EmailStr
    phone_number: str | None = None
    location: str | None = None
    sent_to: str | None = None
    total_points: int
    remaining_points: int | None = None
    assigned_points_total: int | None = None
    available_points: int | None = None
    status: str
    product_image_url: str | None = None
    created_at: datetime
    updated_at: datetime


class RewardClaimListResponse(BaseModel):
    total: int
    claims: list[RewardClaimOut]


class MemberSessionProgressSaveRequest(BaseModel):
    model_config = {"populate_by_name": True}

    user_id: int = Field(..., ge=1, description="Member user id")
    week_id: int | None = Field(default=None, ge=1, validation_alias=AliasChoices("week_id", "weekId"))
    week_name: str | None = Field(default=None, validation_alias=AliasChoices("week_name", "weekName"))
    week_index: int | None = Field(default=None, ge=0, validation_alias=AliasChoices("week_index", "weekIndex"))
    session_id: int | None = Field(default=None, ge=1, validation_alias=AliasChoices("session_id", "sessionId"))
    session_name: str | None = Field(default=None, validation_alias=AliasChoices("session_name", "sessionName"))
    session_number: int | None = Field(
        default=None,
        ge=1,
        validation_alias=AliasChoices("session_number", "sessionNumber"),
    )
    assigned_points: int = Field(
        default=0,
        ge=0,
        validation_alias=AliasChoices("assigned_points", "assignedPoints", "points"),
    )
    feedback: str | None = None
    is_completed: bool = Field(
        default=False,
        validation_alias=AliasChoices("is_completed", "isCompleted", "completed"),
    )
    add_to_existing: bool = Field(
        default=False,
        validation_alias=AliasChoices("add_to_existing", "addToExisting", "increment_points", "incrementPoints"),
    )


class MemberSessionProgressOut(BaseModel):
    id: int
    user_id: int
    week_id: int | None = None
    week_name: str | None = None
    week_index: int | None = None
    session_id: int | None = None
    session_name: str | None = None
    session_number: int | None = None
    assigned_points: int
    feedback: str | None = None
    image_url: str | None = None
    image_urls: list[str] = Field(default_factory=list)
    is_completed: bool
    assigned_by_id: int | None = None
    assigned_by_name: str | None = None
    assigned_points_total: int | None = None
    available_points: int | None = None
    created_at: datetime
    updated_at: datetime


class MemberSessionProgressListResponse(BaseModel):
    total: int
    progress: list[MemberSessionProgressOut]


class UserDashboardStatOut(BaseModel):
    label: str
    value: str | int
    helper_text: str | None = None


class UserDashboardLastProgressOut(BaseModel):
    id: int | None = None
    week_id: int | None = None
    week_name: str | None = None
    week_index: int | None = None
    session_id: int | None = None
    session_name: str | None = None
    session_number: int | None = None
    assigned_points: int = 0
    feedback: str | None = None
    image_url: str | None = None
    is_completed: bool = False
    updated_at: datetime | None = None


class UserDashboardModuleProgressOut(BaseModel):
    module_id: int
    module_kind: str
    module_name: str
    completed_sessions: int
    total_sessions: int
    progress_percent: int
    total_weeks: int


class UserDashboardResponse(BaseModel):
    user_id: int
    full_name: str
    level_id: int | None = None
    level_slug: str | None = None
    level_name: str | None = None
    total_points: int
    assigned_points_total: int
    available_points: int
    completed_sessions: int
    total_sessions: int
    last_progress_percent: int
    stats: list[UserDashboardStatOut]
    current_level_progress: list[UserDashboardModuleProgressOut]
    last_progress: UserDashboardLastProgressOut | None = None


# --- Admin dashboard ---
class DashboardSummaryOut(BaseModel):
    total_members: int
    sessions_completed: int
    total_badges: int
    total_community: int


class DashboardChartOut(BaseModel):
    period: str | None = None
    labels: list[str]
    values: list[int]


class DashboardActivityOut(BaseModel):
    id: int
    title: str
    description: str
    post_type: str
    media_url: str | None = None
    has_media: bool = False
    created_at: datetime
    created_at_label: str | None = None
    created_by_name: str | None = None


class AdminDashboardResponse(BaseModel):
    period: str
    summary: DashboardSummaryOut
    session_completions: DashboardChartOut
    members_by_level: DashboardChartOut
    recent_community_activity: list[DashboardActivityOut]


# --- Member notifications (admin) ---
class SendMemberNotificationRequest(BaseModel):
    user_id: int | None = Field(
        default=None,
        ge=1,
        description="Member to notify. Omit or null to send to all active members.",
    )
    notification_type: str = Field(
        ...,
        description="One of: announcement, reward, feedback, system, achievement",
    )
    title: str = Field(..., min_length=1, max_length=255)
    message: str = Field(..., min_length=1)


class SendMemberNotificationResponse(BaseModel):
    message: str
    recipients_count: int
    notification_type: str
    title: str
    recipient_label: str


class NotificationRecipientOut(BaseModel):
    id: int
    full_name: str
    email: str


class NotificationRecipientListResponse(BaseModel):
    total: int
    members: list[NotificationRecipientOut]


class MemberNotificationOut(BaseModel):
    id: int
    user_id: int
    user_name: str
    recipient: str
    notification_type: str
    type_label: str
    title: str
    message: str
    is_read: bool
    admin_cleared: bool = False
    sent_by_id: int | None = None
    sent_by_name: str | None = None
    created_at: datetime
    created_at_label: str | None = None


class MemberNotificationListResponse(BaseModel):
    total: int
    notifications: list[MemberNotificationOut]


class MemberNotificationIdRequest(BaseModel):
    notification_id: int = Field(..., ge=1, description="member_notifications.id")


class MemberNotificationUnreadCountResponse(BaseModel):
    unread_count: int


# --- Admin activity notifications (member → admin) ---
class AdminNotificationOut(BaseModel):
    id: int
    user_id: int
    user_name: str
    member_name: str
    notification_type: str
    type_label: str
    title: str
    message: str
    reference_type: str | None = None
    reference_id: int | None = None
    is_read: bool
    created_at: datetime
    created_at_label: str | None = None


class AdminNotificationListResponse(BaseModel):
    total: int
    notifications: list[AdminNotificationOut]


class AdminNotificationIdRequest(BaseModel):
    notification_id: int = Field(..., ge=1, description="admin_notifications.id")


class AdminNotificationUnreadCountResponse(BaseModel):
    unread_count: int


# --- Content management (admin) ---
class ContentPageOut(BaseModel):
    content_type: str
    title: str
    content: str
    last_updated: datetime
    last_updated_label: str


class SaveContentRequest(BaseModel):
    content_type: str = Field(..., description="privacy_policy or terms_conditions")
    title: str = Field(..., min_length=1, max_length=255)
    content: str = Field(..., min_length=1)


class FaqOut(BaseModel):
    id: int
    question: str
    answer: str
    status: str
    status_label: str
    view_count: int
    last_updated: datetime
    last_updated_label: str
    created_at: datetime


class FaqListResponse(BaseModel):
    total: int
    skip: int
    limit: int
    faqs: list[FaqOut]


class FaqCreateRequest(BaseModel):
    question: str = Field(..., min_length=1, max_length=512)
    answer: str = Field(..., min_length=1)
    status: str | None = Field(default="completed", description="completed or draft")


class FaqUpdateRequest(BaseModel):
    faq_id: int = Field(..., ge=1)
    question: str = Field(..., min_length=1, max_length=512)
    answer: str = Field(..., min_length=1)
    status: str | None = Field(default="completed")


class FaqIdRequest(BaseModel):
    faq_id: int = Field(..., ge=1)
