"""Author: Vasiliy Zdanovskiy
email: vasilyvz@gmail.com

Transfer session metadata models and JSON-serializable dataclasses.
"""

from __future__ import annotations

import re
from dataclasses import asdict, dataclass
from enum import Enum
from typing import Any, Mapping

SHA256_HEX_LENGTH: int = 64
CHECKSUM_HEX_PATTERN: re.Pattern[str] = re.compile(r"^[0-9a-f]+$")


class TransferStatus(str, Enum):
    CREATED = "created"
    UPLOADING = "uploading"
    UPLOADED = "uploaded"
    DOWNLOADING = "downloading"
    COMPLETED = "completed"
    FAILED = "failed"
    EXPIRED = "expired"
    CONSUMED = "consumed"


def _require_session_key(payload: Mapping[str, Any], key: str) -> Any:
    if key not in payload:
        raise ValueError(f"Missing key {key!r} in TransferSession payload")
    return payload[key]


@dataclass
class TransferSession:
    transfer_id: str
    filename: str
    direction: str
    size_bytes: int
    checksum_value: str
    chunk_size: int
    status: str
    storage_path: str
    metadata_path: str
    checksum_algorithm: str = "sha256"
    compression: str = "identity"
    offset: int = 0
    expires_at: str | None = None
    error_message: str | None = None
    compressed_storage_path: str | None = None
    plaintext_size_bytes: int | None = None
    job_id: str | None = None
    correlation_id: str | None = None
    command: str | None = None
    inbound_transfer_id: str | None = None
    delete_source_on_ack: bool = False

    def to_dict(self) -> dict[str, object]:
        return {
            "transfer_id": self.transfer_id,
            "filename": self.filename,
            "direction": self.direction,
            "size_bytes": self.size_bytes,
            "checksum_algorithm": self.checksum_algorithm,
            "checksum_value": self.checksum_value,
            "compression": self.compression,
            "chunk_size": self.chunk_size,
            "offset": self.offset,
            "status": self.status,
            "storage_path": self.storage_path,
            "metadata_path": self.metadata_path,
            "expires_at": self.expires_at,
            "error_message": self.error_message,
            "compressed_storage_path": self.compressed_storage_path,
            "plaintext_size_bytes": self.plaintext_size_bytes,
            "job_id": self.job_id,
            "correlation_id": self.correlation_id,
            "command": self.command,
            "inbound_transfer_id": self.inbound_transfer_id,
            "delete_source_on_ack": self.delete_source_on_ack,
        }

    @classmethod
    def from_dict(cls, payload: Mapping[str, Any]) -> TransferSession:
        transfer_id = _require_session_key(payload, "transfer_id")
        filename = _require_session_key(payload, "filename")
        direction = _require_session_key(payload, "direction")
        size_bytes_raw = _require_session_key(payload, "size_bytes")
        checksum_value = _require_session_key(payload, "checksum_value")
        chunk_size_raw = _require_session_key(payload, "chunk_size")
        status = _require_session_key(payload, "status")
        storage_path = _require_session_key(payload, "storage_path")
        metadata_path = _require_session_key(payload, "metadata_path")

        checksum_algorithm = (
            "sha256"
            if "checksum_algorithm" not in payload
            else payload["checksum_algorithm"]
        )
        compression = (
            "identity" if "compression" not in payload else payload["compression"]
        )
        offset_raw = 0 if "offset" not in payload else payload["offset"]
        expires_at = None if "expires_at" not in payload else payload["expires_at"]
        error_message = (
            None if "error_message" not in payload else payload["error_message"]
        )
        compressed_storage_path = (
            None
            if "compressed_storage_path" not in payload
            or payload["compressed_storage_path"] is None
            else payload["compressed_storage_path"]
        )
        plaintext_size_raw = (
            None
            if "plaintext_size_bytes" not in payload
            or payload["plaintext_size_bytes"] is None
            else payload["plaintext_size_bytes"]
        )
        job_id = None if "job_id" not in payload else payload["job_id"]
        if job_id is not None and not isinstance(job_id, str):
            raise ValueError("job_id must be a string when set")
        correlation_id = (
            None
            if "correlation_id" not in payload
            else payload["correlation_id"]
        )
        if correlation_id is not None and not isinstance(correlation_id, str):
            raise ValueError("correlation_id must be a string when set")
        command = None if "command" not in payload else payload["command"]
        if command is not None and not isinstance(command, str):
            raise ValueError("command must be a string when set")
        inbound_transfer_id = (
            None
            if "inbound_transfer_id" not in payload
            else payload["inbound_transfer_id"]
        )
        if inbound_transfer_id is not None and not isinstance(
            inbound_transfer_id, str
        ):
            raise ValueError("inbound_transfer_id must be a string when set")
        delete_source_on_ack_raw = payload.get("delete_source_on_ack", False)
        delete_source_on_ack = bool(delete_source_on_ack_raw)

        try:
            size_bytes = int(size_bytes_raw)
            chunk_size = int(chunk_size_raw)
            offset = int(offset_raw)
        except (ValueError, TypeError) as exc:
            raise ValueError("TransferSession integer fields must be integers") from exc

        if size_bytes < 0:
            raise ValueError("size_bytes must be >= 0")
        if direction not in ("upload", "download"):
            raise ValueError('direction must be "upload" or "download"')
        if checksum_algorithm != "sha256":
            raise ValueError('checksum_algorithm must be "sha256"')
        if not isinstance(checksum_value, str) or not checksum_value:
            raise ValueError(
                "checksum_value must be a 64-char lowercase hex sha256 digest"
            )
        if not CHECKSUM_HEX_PATTERN.fullmatch(checksum_value):
            raise ValueError(
                "checksum_value must be a 64-char lowercase hex sha256 digest"
            )
        if len(checksum_value) != SHA256_HEX_LENGTH:
            raise ValueError(
                "checksum_value must be a 64-char lowercase hex sha256 digest"
            )
        if compression not in ("identity", "gzip"):
            raise ValueError('compression must be "identity" or "gzip"')
        if chunk_size <= 0:
            raise ValueError("chunk_size must be > 0")
        if not 0 <= offset <= size_bytes:
            raise ValueError("offset must satisfy 0 <= offset <= size_bytes")
        valid_statuses = {e.value for e in TransferStatus}
        if status not in valid_statuses:
            raise ValueError("invalid TransferStatus value")

        if compressed_storage_path is not None and not isinstance(
            compressed_storage_path, str
        ):
            raise ValueError("compressed_storage_path must be a string when set")
        plaintext_size_bytes: int | None
        if plaintext_size_raw is None:
            plaintext_size_bytes = None
        else:
            try:
                plaintext_size_bytes = int(plaintext_size_raw)
            except (ValueError, TypeError) as exc:
                raise ValueError(
                    "plaintext_size_bytes must be an integer when set"
                ) from exc
            if plaintext_size_bytes < 0:
                raise ValueError("plaintext_size_bytes must be >= 0 when set")

        return cls(
            transfer_id=transfer_id,
            filename=filename,
            direction=direction,
            size_bytes=size_bytes,
            checksum_value=checksum_value,
            chunk_size=chunk_size,
            status=status,
            storage_path=storage_path,
            metadata_path=metadata_path,
            checksum_algorithm=checksum_algorithm,
            compression=compression,
            offset=offset,
            expires_at=expires_at,
            error_message=error_message,
            compressed_storage_path=compressed_storage_path,
            plaintext_size_bytes=plaintext_size_bytes,
            job_id=job_id,
            correlation_id=correlation_id,
            command=command,
            inbound_transfer_id=inbound_transfer_id,
            delete_source_on_ack=delete_source_on_ack,
        )


@dataclass(frozen=True)
class TransferChunk:
    transfer_id: str
    offset: int
    length: int
    checksum_algorithm: str
    checksum_value: str | None = None

    def to_dict(self) -> dict[str, object]:
        return dict(asdict(self))


@dataclass(frozen=True)
class TransferReceipt:
    transfer_id: str
    filename: str
    size_bytes: int
    checksum_algorithm: str
    checksum_value: str
    compression: str
    chunk_size: int
    offset: int
    status: str
    completed: bool
    expires_at: str | None
    plaintext_size_bytes: int | None = None

    def to_dict(self) -> dict[str, object]:
        return dict(asdict(self))


@dataclass(frozen=True)
class TransferDownloadRequest:
    transfer_id: str
    destination_path: str
    chunk_size: int | None = None
    resume: bool = False

    def to_dict(self) -> dict[str, object]:
        return dict(asdict(self))


@dataclass(frozen=True)
class TransferDownloadReceipt:
    transfer_id: str
    destination_path: str
    offset: int
    size_bytes: int
    checksum_algorithm: str
    checksum_value: str
    compression: str
    status: str
    completed: bool
    checksum_verified: bool

    def to_dict(self) -> dict[str, object]:
        return dict(asdict(self))
