#!/usr/bin/env python3
"""
Utility di libreria: smb_util

Un modulo Python riutilizzabile per connettersi a una share SMB e svolgere operazioni di upload/download.

Dipendenze:
    pip install pysmb

API principale:
- SMBClient(auth: SMBAuth, server: SMBServer) -> client con metodi:
    * connect() / close()  oppure context manager: `with SMBClient(...) as c:`
    * list_shares() -> list[str]
    * upload_file(share: str, local_path: str | Path, remote_path: str)
    * download_file(share: str, remote_path: str, local_path: str | Path)

Note:
- Niente I/O su stdin: la password va passata via codice (o `os.environ["SMB_PASSWORD"]`).
- Crea automaticamente cartelle remote/locali se mancanti.
- Di default usa TCP diretto 445 e NTLMv2.
"""
from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path
from typing import Iterable, Optional, Tuple
import os

from smb.SMBConnection import SMBConnection
from smb.smb_structs import OperationFailure


@dataclass(frozen=True)
class SMBAuth:
    username: str
    password: str
    domain: str = ""
    client_name: Optional[str] = None  # se None: hostname locale


@dataclass(frozen=True)
class SMBServer:
    server_name: str  # NetBIOS server name (es. FILESERVER)
    server_ip: str  # es. 192.168.1.10
    port: int = 445
    timeout: int = 30
    use_ntlm_v2: bool = True
    is_direct_tcp: bool = True


class SMBClient:
    """Client SMB semplice, adatto all'uso in codice e come context manager.

    Esempio:
        >>> auth = SMBAuth(username="alext", password=os.environ["SMB_PASSWORD"], domain="WORKGROUP")
        >>> srv = SMBServer(server_name="FILESERVER", server_ip="192.168.0.1")
        >>> with SMBClient(auth, srv) as c:
        ...     c.upload_file("Dati", "./relazione.pdf", "/Progetti/2025/relazione.pdf")
    """

    def __init__(self, auth: SMBAuth, server: SMBServer):
        self._auth = auth
        self._server = server
        self._conn: Optional[SMBConnection] = None

    # -------------------- lifecycle --------------------
    def connect(self) -> None:
        if self._conn is not None:
            return
        client_name = self._auth.client_name or os.uname().nodename
        self._conn = SMBConnection(
            self._auth.username,
            self._auth.password,
            client_name,
            self._server.server_name,
            domain=self._auth.domain,
            use_ntlm_v2=self._server.use_ntlm_v2,
            is_direct_tcp=self._server.is_direct_tcp,
        )
        ok = self._conn.connect(self._server.server_ip, port=self._server.port, timeout=self._server.timeout)
        if not ok:
            self._conn = None
            raise ConnectionError("Connessione SMB fallita: connect() ha restituito False")

    def close(self) -> None:
        if self._conn is not None:
            try:
                self._conn.close()
            finally:
                self._conn = None

    def __enter__(self) -> "SMBClient":
        self.connect()
        return self

    def __exit__(self, exc_type, exc, tb) -> None:
        self.close()

    # -------------------- helpers --------------------
    @staticmethod
    def _normalize_slashes(path: str) -> str:
        return path.replace("\\", "/")

    @staticmethod
    def _normalize_remote_path(remote_path: str) -> str:
        p = SMBClient._normalize_slashes(remote_path)
        while "//" in p:
            p = p.replace("//", "/")
        if p.startswith("/"):
            p = p[1:]
        if p.endswith("/"):
            p = p[:-1]
        return p

    @staticmethod
    def _split_remote_dir_file(remote_path: str) -> tuple[str, str]:
        p = SMBClient._normalize_remote_path(remote_path)
        if "/" in p:
            dir_part, file_part = p.rsplit("/", 1)
        else:
            dir_part, file_part = "", p
        if not file_part:
            raise ValueError("remote_path deve includere anche il nome file")
        return dir_part, file_part

    @staticmethod
    def _parse_share_and_path(remote_path: str, share_hint: Optional[str] = None) -> Tuple[str, str]:
        """Restituisce (share, path_relativo_alla_share) accettando:
        - UNC completo: "//HOST/SHARE/dir/file" oppure "\\\\HOST\\SHARE\\dir\\file"
        - Con prefisso share: "/SHARE/dir/file"
        - Solo percorso relativo + share_hint separato
        Se sia UNC che share_hint sono forniti, share_hint viene ignorato.
        L'host nel percorso UNC viene ignorato: si usa la connessione già aperta.
        """
        raw = SMBClient._normalize_slashes(remote_path).strip()
        if raw.startswith("//"):  # UNC
            parts = [p for p in raw[2:].split("/") if p]
            if len(parts) < 2:
                raise ValueError("Percorso UNC incompleto: serve almeno //HOST/SHARE/file")
            _host = parts[0]
            share = parts[1]
            inner = "/".join(parts[2:])
            if not inner:
                raise ValueError("Percorso UNC deve includere anche il nome file dopo la share")
            return share, SMBClient._normalize_remote_path(inner)
        # Caso: "/SHARE/dir/file" quando non si conosce a priori la share
        if raw.startswith("/") and not share_hint:
            parts = [p for p in raw.split("/") if p]
            if not parts:
                raise ValueError("Percorso remoto vuoto")
            share = parts[0]
            inner = "/".join(parts[1:])
            if not inner:
                raise ValueError("Il percorso deve includere un file dopo il nome della share")
            return share, SMBClient._normalize_remote_path(inner)
        # Caso: share fornita e remote_path forse contiene il prefisso share
        if share_hint:
            rp = SMBClient._normalize_remote_path(raw)
            s = share_hint.strip("/\\")
            if rp.lower().startswith(s.lower() + "/"):
                rp = rp[len(s) + 1:]
            return s, rp
        # Nessuna share ricavabile
        raise ValueError(
            "Serve una share (parametro 'share') oppure un percorso che includa la share, es. '/SHARE/file' o '//HOST/SHARE/file'.")

    def _ensure_connection(self) -> SMBConnection:
        if self._conn is None:
            raise RuntimeError("Client non connesso: chiamare connect() o usare il context manager")
        return self._conn

    # -------------------- features --------------------
    def list_shares(self) -> list[str]:
        """Ritorna l'elenco delle share non speciali."""
        conn = self._ensure_connection()
        return [s.name for s in conn.listShares() if not s.isSpecial]

    def ensure_remote_dirs(self, share: str, remote_dir: str) -> None:
        """Crea ricorsivamente le directory sulla share se non esistono."""
        conn = self._ensure_connection()
        remote_dir = self._normalize_remote_path(remote_dir)
        if not remote_dir:
            return
        parts = remote_dir.split("/")
        current = ""
        for part in parts:
            current = f"{current}/{part}" if current else part
            try:
                conn.listPath(share, current)
            except OperationFailure:
                conn.createDirectory(share, current)

    def upload_file(self, share: Optional[str], local_path: str | Path, remote_path: str) -> None:
        """Carica un file locale.

        Accetta sia `share` esplicita + `remote_path` relativo, sia `remote_path` con share inclusa
        (es. "/SHARE/dir/file" oppure "//HOST/SHARE/dir/file"). In quest'ultimo caso `share` può essere None.
        """
        conn = self._ensure_connection()
        lp = Path(local_path)
        if not lp.is_file():
            raise FileNotFoundError(f"File locale inesistente o non file: {lp}")
        share_name, inner = self._parse_share_and_path(remote_path, share)
        dir_part, file_part = self._split_remote_dir_file(inner)
        if dir_part:
            self.ensure_remote_dirs(share_name, dir_part)
        remote_full = f"{dir_part + '/' + file_part if dir_part else file_part}"
        with lp.open("rb") as fh:
            conn.storeFile(share_name, remote_full, fh)

    def download_file(self, share: Optional[str], remote_path: str, local_path: str | Path) -> Path:
        """Scarica un file remoto su un percorso locale.

        Puoi passare:
        - share=None e remote_path come "/SHARE/dir/file" o "//HOST/SHARE/dir/file"
        - oppure share="SHARE" e remote_path relativo (con o senza prefisso SHARE/)
        """
        conn = self._ensure_connection()
        lp = Path(local_path)
        share_name, inner = self._parse_share_and_path(remote_path, share)
        dir_part, file_part = self._split_remote_dir_file(inner)
        if lp.exists() and lp.is_dir():
            lp = lp / file_part
        lp.parent.mkdir(parents=True, exist_ok=True)
        remote_full = f"{dir_part + '/' + file_part if dir_part else file_part}"
        with lp.open("wb") as fh:
            conn.retrieveFile(share_name, remote_full, fh)
        return lp


__all__ = [
    "SMBAuth",
    "SMBServer",
    "SMBClient",
]

# # -------------------- Esempio d'uso (main) --------------------
# if __name__ == "__main__":
#     # Esempio integrato: usa password da env o costante
#     auth = SMBAuth(
#         username=os.getenv("SMB_USER", "alext"),
#         password=os.getenv("SMB_PASSWORD", ""),
#         # domain="WORKGROUP",
#     )
#
#     server = SMBServer(server_name=os.getenv("SMB_SERVER_NAME", "FILESERVER"),
#                        server_ip=os.getenv("SMB_SERVER_IP", "192.168.0.1"))
#
#     client = SMBClient(auth, server)
#
#     client.connect()
#
#     client.upload_file("TEMP", remote_path="/TEMP/json_ale_upload.txt", local_path="./ciao_unc.txt")
#
#     client.close()
