from __future__ import annotations

from datetime import datetime
from typing import Any

from pydantic import BaseModel, Field, field_validator, model_validator

from flow_xhs.contracts.command_rules import (
    ACCOUNT_LOGIN_MODE_VALUES,
    ACCOUNT_LOGIN_TIMEOUT_DEFAULT,
    ACCOUNT_LOGIN_TIMEOUT_MIN,
    BOOLEAN_LITERAL_VALUES,
    NOTE_GET_COMMENT_SIZE_DEFAULT,
    NOTE_GET_COMMENT_SIZE_MIN,
    NOTE_GET_SUB_COMMENT_SIZE_DEFAULT,
    NOTE_GET_SUB_COMMENT_SIZE_MIN,
    NOTE_XSEC_SOURCE_VALUES,
    NOTE_SEARCH_SIZE_DEFAULT,
    PUBLISH_TARGET_VALUES,
    USER_SEARCH_SIZE_MIN,
    NOTE_SEARCH_SIZE_MIN,
    USER_XSEC_SOURCE_VALUES,
    USER_SEARCH_SIZE_DEFAULT,
)
from flow_xhs.platform.search_filters import normalize_filter_value
from flow_xhs.runtime.rednote_urls import parse_note_url, parse_user_url


class CommonInput(BaseModel):
    trace_id: str | None = None
    account_uid: str | None = None


class NoteSearchInput(CommonInput):
    keyword: str = Field(min_length=1)
    size: int = Field(default=NOTE_SEARCH_SIZE_DEFAULT, ge=NOTE_SEARCH_SIZE_MIN)
    sort_by: str | None = None
    note_type: str | None = None
    publish_time: str | None = None
    search_scope: str | None = None
    location: str | None = None

    @field_validator("sort_by", mode="before")
    @classmethod
    def _normalize_sort_by(cls, value: str | None) -> str | None:
        return normalize_filter_value("sort_by", value)

    @field_validator("note_type", mode="before")
    @classmethod
    def _normalize_note_type(cls, value: str | None) -> str | None:
        return normalize_filter_value("note_type", value)

    @field_validator("publish_time", mode="before")
    @classmethod
    def _normalize_publish_time(cls, value: str | None) -> str | None:
        return normalize_filter_value("publish_time", value)

    @field_validator("search_scope", mode="before")
    @classmethod
    def _normalize_search_scope(cls, value: str | None) -> str | None:
        return normalize_filter_value("search_scope", value)

    @field_validator("location", mode="before")
    @classmethod
    def _normalize_location(cls, value: str | None) -> str | None:
        return normalize_filter_value("location", value)


class UserSearchInput(CommonInput):
    keyword: str = Field(min_length=1)
    size: int = Field(default=USER_SEARCH_SIZE_DEFAULT, ge=USER_SEARCH_SIZE_MIN)


class UserGetInput(CommonInput):
    user_url: str | None = None
    user_id: str | None = Field(default=None, min_length=1)
    xsec_token: str | None = None
    xsec_source: str | None = "pc_feed"

    @model_validator(mode="before")
    @classmethod
    def _resolve_user_url_fields(cls, data: Any) -> Any:
        if not isinstance(data, dict):
            return data
        values = dict(data)
        _reject_legacy_fields(values, field_names=("user_id", "xsec_token", "xsec_source"))
        raw_user_url = values.get("user_url")
        if raw_user_url is None:
            return values
        parsed = parse_user_url(raw_user_url)
        _merge_derived_field(values, field_name="user_id", derived_value=parsed.user_id, source_field="user_url")
        _merge_derived_field(values, field_name="xsec_token", derived_value=parsed.xsec_token, source_field="user_url")
        _merge_derived_field(values, field_name="xsec_source", derived_value=parsed.xsec_source, source_field="user_url")
        return values

    @field_validator("xsec_source", mode="before")
    @classmethod
    def _normalize_user_xsec_source(cls, value: str | None) -> str | None:
        return _normalize_literal_choice(
            field_name="xsec_source",
            value=value,
            allowed_values=USER_XSEC_SOURCE_VALUES,
        )

    @model_validator(mode="after")
    def _require_user_locator(self) -> "UserGetInput":
        if not self.user_url:
            raise ValueError("需要提供 user_url")
        return self


class NoteGetInput(CommonInput):
    note_url: str | None = None
    note_id: str | None = Field(default=None, min_length=1)
    xsec_token: str | None = None
    xsec_source: str | None = "pc_feed"
    comment_size: int = Field(
        default=NOTE_GET_COMMENT_SIZE_DEFAULT,
        ge=NOTE_GET_COMMENT_SIZE_MIN,
    )
    sub_comment_size: int = Field(
        default=NOTE_GET_SUB_COMMENT_SIZE_DEFAULT,
        ge=NOTE_GET_SUB_COMMENT_SIZE_MIN,
    )

    @model_validator(mode="before")
    @classmethod
    def _resolve_note_url_fields(cls, data: Any) -> Any:
        if not isinstance(data, dict):
            return data
        values = dict(data)
        _reject_legacy_fields(values, field_names=("note_id", "xsec_token", "xsec_source"))
        raw_note_url = values.get("note_url")
        if raw_note_url is None:
            return values
        parsed = parse_note_url(raw_note_url)
        _merge_derived_field(values, field_name="note_id", derived_value=parsed.note_id, source_field="note_url")
        _merge_derived_field(values, field_name="xsec_token", derived_value=parsed.xsec_token, source_field="note_url")
        _merge_derived_field(values, field_name="xsec_source", derived_value=parsed.xsec_source, source_field="note_url")
        return values

    @field_validator("xsec_source", mode="before")
    @classmethod
    def _normalize_note_xsec_source(cls, value: str | None) -> str | None:
        return _normalize_literal_choice(
            field_name="xsec_source",
            value=value,
            allowed_values=NOTE_XSEC_SOURCE_VALUES,
        )

    @model_validator(mode="after")
    def _require_note_locator(self) -> "NoteGetInput":
        if not self.note_url:
            raise ValueError("需要提供 note_url")
        return self


class PublishNoteInput(CommonInput):
    target: str = Field(min_length=1)
    image_list: list[str] = Field(default_factory=list)
    video: str | None = None
    title: str
    content: str
    tags: list[str] = Field(default_factory=list)
    schedule_at: str | None = None

    @field_validator("target", mode="before")
    @classmethod
    def _normalize_target(cls, value: str) -> str:
        if not isinstance(value, str):
            raise ValueError("target 必须是字符串")
        text = value.strip().lower()
        if not text:
            raise ValueError("target 不能为空")
        if text not in PUBLISH_TARGET_VALUES:
            raise ValueError(f"target 不支持 '{text}'，可选值: {', '.join(PUBLISH_TARGET_VALUES)}")
        return text

    @field_validator("image_list")
    @classmethod
    def _validate_images(cls, value: list[str]) -> list[str]:
        cleaned = [v.strip() for v in value if isinstance(v, str) and v.strip()]
        return cleaned

    @field_validator("video", mode="before")
    @classmethod
    def _validate_video(cls, value: str | None) -> str | None:
        if value is None:
            return None
        if not isinstance(value, str):
            raise ValueError("video 必须是字符串")
        text = value.strip()
        return text or None

    @field_validator("title", "content", mode="before")
    @classmethod
    def _validate_required_text(cls, value: str | None, info) -> str:
        if value is None:
            raise ValueError(f"{info.field_name} 不能为空")
        if not isinstance(value, str):
            raise ValueError(f"{info.field_name} 必须是字符串")
        text = value.strip()
        if not text:
            raise ValueError(f"{info.field_name} 不能为空")
        return text

    @field_validator("schedule_at", mode="before")
    @classmethod
    def _validate_schedule_at(cls, value: str | None) -> str | None:
        if value is None:
            return None
        if not isinstance(value, str):
            raise ValueError("schedule_at 必须是字符串")
        text = value.strip()
        if not text:
            return None
        PublishScheduleModel(schedule_at=text)
        return text

    @model_validator(mode="after")
    def _validate_publish_target_media(self) -> "PublishNoteInput":
        target_text = self.target.strip().lower()
        if target_text == "image" and not self.image_list:
            raise ValueError("image_list 至少包含一个有效项")
        if target_text == "video" and not self.video:
            raise ValueError("video 不能为空")
        return self


class AccountListInput(CommonInput):
    only_active: bool = True

    @field_validator("only_active", mode="before")
    @classmethod
    def _normalize_only_active(cls, value: bool | str) -> bool:
        normalized = _normalize_literal_choice(
            field_name="only_active",
            value=value,
            allowed_values=BOOLEAN_LITERAL_VALUES,
            allow_none=False,
        )
        return normalized == "true"


class AuthLoginInput(CommonInput):
    account_uid: str | None = None
    mode: str = "local"
    login_timeout_seconds: int = Field(default=ACCOUNT_LOGIN_TIMEOUT_DEFAULT, ge=ACCOUNT_LOGIN_TIMEOUT_MIN)
    screenshot_path: str = ""

    @field_validator("mode", mode="before")
    @classmethod
    def _normalize_mode(cls, value: str) -> str:
        normalized = _normalize_literal_choice(
            field_name="mode",
            value=value,
            allowed_values=ACCOUNT_LOGIN_MODE_VALUES,
            allow_none=False,
        )
        if normalized is None:
            raise ValueError("mode 不能为空")
        return normalized

    @field_validator("account_uid", mode="before")
    @classmethod
    def _strip_optional_account_uid(cls, value: str | None) -> str | None:
        if value is None:
            return None
        if not isinstance(value, str):
            raise ValueError("字段必须是字符串")
        return value.strip() or None

    @field_validator("screenshot_path", mode="before")
    @classmethod
    def _strip_screenshot_path(cls, value: str | None) -> str:
        if value is None:
            return ""
        if not isinstance(value, str):
            raise ValueError("字段必须是字符串")
        return value.strip()


class GenericInputEnvelope(BaseModel):
    command: str
    params: dict[str, Any]
    trace_id: str | None = None
    account_uid: str | None = None


class PublishScheduleModel(BaseModel):
    schedule_at: datetime


def _normalize_literal_choice(
    *,
    field_name: str,
    value: str | bool | None,
    allowed_values: tuple[str, ...],
    allow_none: bool = True,
) -> str | None:
    if value is None:
        if allow_none:
            return None
        raise ValueError(f"{field_name} 不能为空")
    if isinstance(value, bool):
        text = "true" if value else "false"
    elif isinstance(value, str):
        text = value.strip().lower()
    else:
        raise ValueError(f"{field_name} 必须是字符串")
    if not text:
        if allow_none:
            return None
        raise ValueError(f"{field_name} 不能为空")
    if text not in allowed_values:
        raise ValueError(
            f"{field_name} 不支持 '{text}'，可选值: {', '.join(allowed_values)}"
        )
    return text


def _merge_derived_field(
    values: dict[str, Any],
    *,
    field_name: str,
    derived_value: str | None,
    source_field: str,
) -> None:
    existing_value = values.get(field_name)
    if derived_value is None:
        return
    if existing_value is None:
        values[field_name] = derived_value
        return

    existing_text = str(existing_value).strip()
    if not existing_text:
        values[field_name] = derived_value
        return
    if field_name == "xsec_source":
        existing_text = existing_text.lower()
    if existing_text != derived_value:
        raise ValueError(f"{field_name} 与 {source_field} 解析结果不一致")


def _reject_legacy_fields(values: dict[str, Any], *, field_names: tuple[str, ...]) -> None:
    for field_name in field_names:
        if field_name not in values:
            continue
        value = values.get(field_name)
        if value is None:
            continue
        if isinstance(value, str) and not value.strip():
            continue
        raise ValueError(f"{field_name} 已废弃，请改用对应的 URL 字段")
