"""Nexus — Pydantic Request/Response Models
Shared schemas for API validation, replacing raw `dict` parameters.
"""

from typing import Optional, List
from pydantic import BaseModel, Field, field_validator, model_validator

from server.api.schema_path_validators import (
    coerce_nexus_host_abs_path,
    coerce_optional_remote_abs_path,
    coerce_remote_abs_path,
    coerce_remote_abs_path_list,
    coerce_remote_relative_path,
)


# ── Setting ──

class SettingUpdatePayload(BaseModel):
    value: str = Field(..., min_length=0)


class ApiKeyRevealRequest(BaseModel):
    """Shared re-auth payload for revealing sensitive values (API key, install cmd, etc.)."""
    current_password: str = Field(..., min_length=1, max_length=255)


# ── Agent ──

class AgentHeartbeat(BaseModel):
    server_id: Optional[int] = None  # None = unregistered agent, silently discard
    is_online: bool = True
    system_info: Optional[dict] = None
    agent_version: Optional[str] = None


# ── Server ──

class ServerCreate(BaseModel):
    name: str = Field(..., min_length=1, max_length=100)
    domain: str = Field(..., min_length=1, max_length=255)
    port: int = Field(22, ge=1, le=65535)
    username: str = Field("root", max_length=100)
    auth_method: str = Field("key", pattern="^(key|password)$")
    password: Optional[str] = None
    preset_id: Optional[int] = None
    ssh_key_path: Optional[str] = None
    ssh_key_private: Optional[str] = None
    ssh_key_public: Optional[str] = None
    ssh_key_preset_id: Optional[int] = None
    agent_port: int = Field(8601, ge=1, le=65535)
    description: Optional[str] = None
    target_path: Optional[str] = None
    category: Optional[str] = None
    platform_id: Optional[int] = None
    node_id: Optional[int] = None
    protocols: Optional[list] = None
    extra_attrs: Optional[dict] = None
    files_elevation: Optional[str] = Field(
        default=None,
        pattern="^(off|auto_sudo|always_sudo)$",
        description="File manager sudo policy (stored in extra_attrs)",
    )

    @field_validator("target_path", mode="before")
    @classmethod
    def _normalize_target_path(cls, v: object) -> object:
        return coerce_optional_remote_abs_path(v if v is None else str(v))


class ServerUpdate(BaseModel):
    name: Optional[str] = Field(None, min_length=1, max_length=100)
    domain: Optional[str] = Field(None, min_length=1, max_length=255)
    port: Optional[int] = Field(None, ge=1, le=65535)
    username: Optional[str] = None
    auth_method: Optional[str] = Field(None, pattern="^(key|password)$")
    password: Optional[str] = None
    preset_id: Optional[int] = None
    ssh_key_path: Optional[str] = None
    ssh_key_private: Optional[str] = None
    ssh_key_public: Optional[str] = None
    ssh_key_preset_id: Optional[int] = None
    agent_port: Optional[int] = Field(None, ge=1, le=65535)
    agent_api_key: Optional[str] = None
    description: Optional[str] = None
    target_path: Optional[str] = None
    category: Optional[str] = None
    platform_id: Optional[int] = None
    node_id: Optional[int] = None
    protocols: Optional[list] = None
    extra_attrs: Optional[dict] = None
    files_elevation: Optional[str] = Field(
        default=None,
        pattern="^(off|auto_sudo|always_sudo)$",
        description="File manager sudo policy (stored in extra_attrs)",
    )

    @field_validator("target_path", mode="before")
    @classmethod
    def _normalize_target_path(cls, v: object) -> object:
        if v is None:
            return None
        return coerce_optional_remote_abs_path(str(v))


class ServerCheck(BaseModel):
    server_ids: List[int] = Field(..., min_length=1)


class ServerImportResult(BaseModel):
    """Result of CSV batch import."""
    total: int = 0
    created: int = 0
    skipped: int = 0
    failed: int = 0
    errors: List[dict] = []  # [{row, name, domain, error}]
    created_ids: List[int] = []


class BatchAgentAction(BaseModel):
    """Batch install/upgrade Agent on multiple servers."""
    server_ids: List[int] = Field(..., min_length=1, max_length=50)


class BatchAgentResultItem(BaseModel):
    server_id: int
    server_name: str
    success: bool
    stdout: str = ""
    error: str = ""


class BatchAgentResult(BaseModel):
    results: List[BatchAgentResultItem] = []


class FileUpload(BaseModel):
    """Upload file to remote server via SFTP."""
    server_id: int = Field(..., ge=1)
    remote_path: str = Field(..., min_length=1)

    @field_validator("remote_path", mode="before")
    @classmethod
    def _normalize_remote_path(cls, v: object) -> str:
        return coerce_remote_abs_path(str(v))


# ── Sync ──

class SyncFiles(BaseModel):
    server_ids: List[int] = Field(..., min_length=1)
    source_path: str = Field(..., min_length=1)
    target_path: Optional[str] = None
    sync_mode: str = Field("incremental", pattern="^(incremental|full|overwrite|checksum)$")
    batch_size: int = Field(50, ge=1, le=200)
    concurrency: int = Field(10, ge=1, le=50)

    @field_validator("source_path", mode="before")
    @classmethod
    def _normalize_source_path(cls, v: object) -> str:
        return coerce_nexus_host_abs_path(str(v))

    @field_validator("target_path", mode="before")
    @classmethod
    def _normalize_target_path(cls, v: object) -> object:
        return coerce_optional_remote_abs_path(v if v is None else str(v))


class SyncPreview(BaseModel):
    """Dry-run: rsync --dry-run --stats against one representative server."""
    server_id: int = Field(..., ge=1)
    source_path: str = Field(..., min_length=1)
    target_path: Optional[str] = None
    sync_mode: str = Field("incremental", pattern="^(incremental|full|overwrite|checksum)$")
    verbose: bool = False  # If True, include per-file list (truncated to 200 lines)

    @field_validator("source_path", mode="before")
    @classmethod
    def _normalize_source_path(cls, v: object) -> str:
        return coerce_nexus_host_abs_path(str(v))

    @field_validator("target_path", mode="before")
    @classmethod
    def _normalize_target_path(cls, v: object) -> object:
        return coerce_optional_remote_abs_path(v if v is None else str(v))


class SyncBrowse(BaseModel):
    server_id: int
    path: str = Field("/", min_length=1)

    @field_validator("path", mode="before")
    @classmethod
    def _normalize_path(cls, v: object) -> str:
        return coerce_remote_abs_path(str(v))


class SyncBrowseLocal(BaseModel):
    """Browse a local directory on the Nexus server (for upload file manager)."""
    path: str = Field(..., min_length=1)

    @field_validator("path", mode="before")
    @classmethod
    def _normalize_path(cls, v: object) -> str:
        return coerce_nexus_host_abs_path(str(v))


class LocalFileOperation(BaseModel):
    """Delete or rename a file/directory in the local upload staging area."""
    operation: str = Field(..., pattern="^(delete|rename)$")
    path: str = Field(..., min_length=1)
    new_name: Optional[str] = None  # required for rename

    @field_validator("path", mode="before")
    @classmethod
    def _normalize_path(cls, v: object) -> str:
        return coerce_nexus_host_abs_path(str(v))


class LocalFilePreview(BaseModel):
    """Preview file content in the local upload staging area."""
    path: str = Field(..., min_length=1)

    @field_validator("path", mode="before")
    @classmethod
    def _normalize_path(cls, v: object) -> str:
        return coerce_nexus_host_abs_path(str(v))


class ValidateSourcePath(BaseModel):
    """Validate a local directory path on the Nexus server as a push source."""
    path: str = Field(..., min_length=1)

    @field_validator("path", mode="before")
    @classmethod
    def _normalize_path(cls, v: object) -> str:
        return coerce_nexus_host_abs_path(str(v))


class SyncDiagnose(BaseModel):
    """Diagnose why a push failed for a specific server."""
    server_id: int = Field(..., ge=1)
    target_path: Optional[str] = None

    @field_validator("target_path", mode="before")
    @classmethod
    def _normalize_target_path(cls, v: object) -> object:
        return coerce_optional_remote_abs_path(v if v is None else str(v))


class FileSyncDiff(BaseModel):
    """Compare a local file with its remote counterpart to show push diff."""
    server_id: int = Field(..., ge=1)
    source_path: str = Field(..., min_length=1)
    relative_path: str = Field(..., min_length=1)
    target_path: Optional[str] = None

    @field_validator("source_path", mode="before")
    @classmethod
    def _normalize_source_path(cls, v: object) -> str:
        return coerce_nexus_host_abs_path(str(v))

    @field_validator("relative_path", mode="before")
    @classmethod
    def _normalize_relative_path(cls, v: object) -> str:
        return coerce_remote_relative_path(str(v))

    @field_validator("target_path", mode="before")
    @classmethod
    def _normalize_target_path(cls, v: object) -> object:
        return coerce_optional_remote_abs_path(v if v is None else str(v))


class SyncCancel(BaseModel):
    """Cancel an in-progress push by batch_id."""
    batch_id: str = Field(..., min_length=1, max_length=32)


class SyncVerify(BaseModel):
    """Post-push verification: compare local source with remote target via md5sum."""
    server_ids: List[int] = Field(..., min_length=1)
    source_path: str = Field(..., min_length=1)
    target_path: Optional[str] = None
    max_files: int = Field(200, ge=1, le=500)

    @field_validator("source_path", mode="before")
    @classmethod
    def _normalize_source_path(cls, v: object) -> str:
        return coerce_nexus_host_abs_path(str(v))

    @field_validator("target_path", mode="before")
    @classmethod
    def _normalize_target_path(cls, v: object) -> object:
        return coerce_optional_remote_abs_path(v if v is None else str(v))


class FileOperation(BaseModel):
    """Remote file operation via SSH exec (delete / rename / mkdir)."""
    server_id: int = Field(..., ge=1)
    operation: str = Field(..., pattern="^(delete|rename|mkdir)$")
    path: str = Field(..., min_length=1)      # source path (or new dir path for mkdir)
    new_path: Optional[str] = None            # required for rename

    @field_validator("path", mode="before")
    @classmethod
    def _normalize_path(cls, v: object) -> str:
        return coerce_remote_abs_path(str(v))

    @field_validator("new_path", mode="before")
    @classmethod
    def _normalize_new_path(cls, v: object) -> object:
        if v is None:
            return None
        return coerce_remote_abs_path(str(v))


class FileClipboardApply(BaseModel):
    """Batch copy or move (cut+paste) remote paths into a destination directory."""
    server_id: int = Field(..., ge=1)
    mode: str = Field(..., pattern="^(copy|move)$")
    sources: list[str] = Field(..., min_length=1, max_length=50)
    dest_dir: str = Field(..., min_length=1, max_length=4096)

    @field_validator("sources", mode="before")
    @classmethod
    def _normalize_sources(cls, v: object) -> list[str]:
        if not isinstance(v, list):
            raise ValueError("sources 必须为路径列表")
        return coerce_remote_abs_path_list([str(x) for x in v])

    @field_validator("dest_dir", mode="before")
    @classmethod
    def _normalize_dest_dir(cls, v: object) -> str:
        return coerce_remote_abs_path(str(v))


class FileDownload(BaseModel):
    """Download a file from a remote server."""
    server_id: int = Field(..., ge=1)
    path: str = Field(..., min_length=1, max_length=4096)

    @field_validator("path", mode="before")
    @classmethod
    def _normalize_path(cls, v: object) -> str:
        return coerce_remote_abs_path(str(v))


class FileRead(BaseModel):
    """Read text file content from a remote server."""
    server_id: int = Field(..., ge=1)
    path: str = Field(..., min_length=1, max_length=4096)
    max_size: int = Field(1_000_000, ge=1, le=5_000_000)  # 1MB default, 5MB max

    @field_validator("path", mode="before")
    @classmethod
    def _normalize_path(cls, v: object) -> str:
        return coerce_remote_abs_path(str(v))


class FileWrite(BaseModel):
    """Write text content to a file on a remote server."""
    server_id: int = Field(..., ge=1)
    path: str = Field(..., min_length=1, max_length=4096)
    content: str = Field(..., max_length=5_000_000)

    @field_validator("path", mode="before")
    @classmethod
    def _normalize_path(cls, v: object) -> str:
        return coerce_remote_abs_path(str(v))


class FileChmod(BaseModel):
    """Change file permissions on a remote server."""
    server_id: int = Field(..., ge=1)
    path: str = Field(..., min_length=1, max_length=4096)
    mode: str = Field(..., min_length=3, max_length=4, pattern=r"^[0-7]{3,4}$")
    owner: Optional[str] = Field(None, max_length=64, pattern=r"^[a-zA-Z0-9_.-]+$")
    group: Optional[str] = Field(None, max_length=64, pattern=r"^[a-zA-Z0-9_.-]+$")
    recursive: bool = False

    @field_validator("path", mode="before")
    @classmethod
    def _normalize_path(cls, v: object) -> str:
        return coerce_remote_abs_path(str(v))

    @model_validator(mode="after")
    def _group_requires_owner(self):
        if self.group and not self.owner:
            raise ValueError("指定属组时必须同时指定属主")
        return self


class FileCompress(BaseModel):
    """Compress files into an archive on a remote server."""
    server_id: int = Field(..., ge=1)
    paths: list[str] = Field(..., min_length=1, max_length=100)
    dest: str = Field(..., min_length=1, max_length=4096)
    format: str = Field("tar.gz", pattern=r"^(tar\.gz|zip)$")

    @field_validator("paths", mode="before")
    @classmethod
    def _normalize_paths(cls, v: object) -> list[str]:
        if not isinstance(v, list):
            raise ValueError("paths 必须为路径列表")
        return coerce_remote_abs_path_list([str(x) for x in v])

    @field_validator("dest", mode="before")
    @classmethod
    def _normalize_dest(cls, v: object) -> str:
        return coerce_remote_abs_path(str(v))


class FileDecompress(BaseModel):
    """Decompress an archive on a remote server."""
    server_id: int = Field(..., ge=1)
    path: str = Field(..., min_length=1, max_length=4096)
    dest: str = Field(..., min_length=1, max_length=4096)

    @field_validator("path", "dest", mode="before")
    @classmethod
    def _normalize_paths(cls, v: object) -> str:
        text = str(v).strip()
        if text == ".":
            raise ValueError("解压目标必须为绝对路径（不可使用 '.'）")
        return coerce_remote_abs_path(text)


# ── Script ──

class ScriptCreate(BaseModel):
    name: str = Field(..., min_length=1, max_length=100)
    category: str = Field("ops", pattern="^(ops|deploy|check|cleanup)$")
    content: str = Field(..., min_length=1)
    description: Optional[str] = None
    created_by: Optional[str] = None


class ScriptUpdate(BaseModel):
    name: Optional[str] = Field(None, min_length=1, max_length=100)
    category: Optional[str] = Field(None, pattern="^(ops|deploy|check|cleanup)$")
    content: Optional[str] = None
    description: Optional[str] = None


class ScriptExecute(BaseModel):
    script_id: Optional[int] = None
    command: str = Field(..., min_length=1)
    server_ids: List[int] = Field(..., min_length=1)
    credential_id: Optional[int] = None
    timeout: int = Field(60, ge=1, le=600)
    long_task: bool = False


class ScriptExecutionRetry(BaseModel):
    server_ids: Optional[List[int]] = None
    credential_id: Optional[int] = None
    long_task: Optional[bool] = None
    timeout: int = Field(120, ge=1, le=600)


class ScriptExecutionMarkStuck(BaseModel):
    detail: str = Field("", max_length=500)


class AgentScriptCallback(BaseModel):
    """Child host reports long-running script completion (curl from nohup wrapper)."""
    job_id: str = Field(..., min_length=8, max_length=64)
    server_id: int = Field(..., ge=1)
    secret: str = Field(..., min_length=16, max_length=128)
    exit_code: int = 0
    message: str = Field("", max_length=500)
    log_tail: Optional[str] = Field(None, max_length=2000)


# ── DB Credential ──

class DbCredentialCreate(BaseModel):
    name: str = Field(..., min_length=1, max_length=100)
    db_type: str = Field("mysql", pattern="^(mysql|postgresql|mariadb|mongodb|redis)$")
    host: str = Field(..., min_length=1)
    port: int = Field(3306, ge=1, le=65535)
    username: str = Field(..., min_length=1)
    password: str = Field(..., min_length=1)
    database: Optional[str] = None


class DbCredentialUpdate(BaseModel):
    name: Optional[str] = Field(None, min_length=1, max_length=100)
    db_type: Optional[str] = Field(None, pattern="^(mysql|postgresql|mariadb|mongodb|redis)$")
    host: Optional[str] = Field(None, min_length=1)
    port: Optional[int] = Field(None, ge=1, le=65535)
    username: Optional[str] = Field(None, min_length=1)
    password: Optional[str] = Field(None, min_length=1, description="留空则不修改密码")
    database: Optional[str] = None


# ── Schedule ──

class ScheduleCreate(BaseModel):
    name: str = Field(..., min_length=1, max_length=100)
    server_ids: str = Field(..., min_length=3)          # JSON array string
    run_mode: str = Field("once", pattern="^(once|cron)$")
    cron_expr: Optional[str] = None                     # required when run_mode=cron
    fire_at: Optional[str] = None                       # ISO datetime, required when run_mode=once
    enabled: bool = True
    # Schedule type
    schedule_type: str = Field("push", pattern="^(push|script)$")
    # Push-specific (required when schedule_type == 'push')
    source_path: Optional[str] = None
    sync_mode: str = Field("incremental", pattern="^(incremental|full|overwrite|checksum)$")
    # Script-specific (required when schedule_type == 'script')
    script_id: Optional[int] = None
    script_content: Optional[str] = None
    exec_timeout: int = Field(60, ge=10, le=600)
    long_task: bool = False

    @model_validator(mode="after")
    def _validate_schedule_fields(self):
        if self.run_mode == "cron" and not self.cron_expr:
            raise ValueError("cron 模式必须提供 cron_expr")
        if self.run_mode == "once" and not self.fire_at:
            raise ValueError("一次性模式必须提供 fire_at 执行时间")
        if self.schedule_type == "script" and not self.script_id and not self.script_content:
            raise ValueError("script 类型调度必须提供 script_id 或 script_content")
        if self.schedule_type == "push" and not self.source_path:
            raise ValueError("push 类型调度必须提供 source_path")
        return self

    @field_validator("source_path", mode="before")
    @classmethod
    def _normalize_source_path(cls, v: object) -> object:
        if v is None:
            return None
        text = str(v).strip()
        if not text:
            return None
        return coerce_nexus_host_abs_path(text)


class ScheduleUpdate(BaseModel):
    name: Optional[str] = Field(None, min_length=1, max_length=100)
    source_path: Optional[str] = None
    server_ids: Optional[str] = None
    run_mode: Optional[str] = Field(None, pattern="^(once|cron)$")
    cron_expr: Optional[str] = None
    fire_at: Optional[str] = None
    sync_mode: Optional[str] = Field(None, pattern="^(incremental|full|overwrite|checksum)$")
    enabled: Optional[bool] = None
    schedule_type: Optional[str] = Field(None, pattern="^(push|script)$")
    script_id: Optional[int] = None
    script_content: Optional[str] = None
    exec_timeout: Optional[int] = Field(None, ge=10, le=600)
    long_task: Optional[bool] = None

    @field_validator("source_path", mode="before")
    @classmethod
    def _normalize_source_path(cls, v: object) -> object:
        if v is None:
            return None
        text = str(v).strip()
        if not text:
            return None
        return coerce_nexus_host_abs_path(text)


# ── Preset ──

class PresetCreate(BaseModel):
    name: str = Field(..., min_length=1, max_length=100)
    encrypted_pw: str = Field(..., min_length=1)


class PresetUpdate(BaseModel):
    name: Optional[str] = Field(None, min_length=1, max_length=100)
    encrypted_pw: Optional[str] = Field(None, min_length=1, description="留空则不修改密码")


# ── SSH Key Preset ──

class SshKeyPresetCreate(BaseModel):
    name: str = Field(..., min_length=1, max_length=100)
    private_key: str = Field(..., min_length=1)
    public_key: Optional[str] = None


class SshKeyPresetUpdate(BaseModel):
    name: Optional[str] = Field(None, min_length=1, max_length=100)
    private_key: Optional[str] = Field(None, min_length=1, description="留空则不修改私钥")
    public_key: Optional[str] = None


# ── Platform / Node ──

class PlatformCreate(BaseModel):
    name: str = Field(..., min_length=1, max_length=100)
    category: str = Field(..., min_length=1, max_length=50)
    type: str = Field(..., min_length=1, max_length=50)
    default_protocols: Optional[list] = None
    charset: str = "utf-8"


class PlatformUpdate(BaseModel):
    name: Optional[str] = Field(None, min_length=1, max_length=100)
    category: Optional[str] = None
    type: Optional[str] = None
    default_protocols: Optional[list] = None
    charset: Optional[str] = None


class NodeCreate(BaseModel):
    name: str = Field(..., min_length=1, max_length=100)
    parent_id: Optional[int] = None
    sort_order: int = 0


class NodeUpdate(BaseModel):
    name: Optional[str] = Field(None, min_length=1, max_length=100)
    parent_id: Optional[int] = None
    sort_order: Optional[int] = None
