"""File upload handler: local filesystem → DMS via qRunX.

Sends chunked file uploads to qRunX over QRP:
  upload_file_start    → announce file metadata
  upload_file_chunk    → send base64-encoded chunks
  upload_file_complete → send checksum for verification

Architecture reference: Section 11.3 (File Transfer Flow).
"""

from __future__ import annotations

import base64
import hashlib
import logging
import os
from typing import Any, Callable, Coroutine

logger = logging.getLogger("qremotex.transfer.uploader")

# Chunk size: ≤ 512 KB of base64 data ≈ 384 KB raw (per Section 5.4)
CHUNK_SIZE_RAW = 384 * 1024  # 384 KB raw bytes → ~512 KB base64

# Maximum upload size (50 MB — matches EXECUTOR_MAX_RESULT_SIZE_MB)
MAX_UPLOAD_SIZE_BYTES = 50 * 1024 * 1024

# Type alias for the send-message callback
SendFn = Callable[[dict[str, Any]], Coroutine[Any, Any, None]]


class FileUploader:
    """Handles outbound file uploads from qRemoteX to DMS via qRunX."""

    def __init__(self, send_fn: SendFn, executor_id: str) -> None:
        self._send = send_fn
        self._executor_id = executor_id

    async def upload(
        self,
        local_path: str,
        target_path: str,
        request_id: str,
    ) -> dict[str, Any]:
        """Upload a local file to DMS via chunked QRP messages.

        Args:
            local_path:   Absolute path to the file on this machine.
            target_path:  Desired DMS path (e.g. "results/output.csv").
            request_id:   The originating execution's request_id for correlation.

        Returns:
            {"ok": True, "file_id": ..., "target_path": ..., "size_bytes": ..., "checksum": ...}
            or {"ok": False, "error": ...}
        """
        # Validate local file
        if not os.path.isfile(local_path):
            return {"ok": False, "error": f"File not found: {local_path}"}

        file_size = os.path.getsize(local_path)
        if file_size > MAX_UPLOAD_SIZE_BYTES:
            return {
                "ok": False,
                "error": f"File too large: {file_size} bytes (max {MAX_UPLOAD_SIZE_BYTES})",
            }

        total_chunks = (file_size + CHUNK_SIZE_RAW - 1) // CHUNK_SIZE_RAW if file_size > 0 else 1

        logger.info(
            "Upload starting: path=%s target=%s size=%d chunks=%d",
            local_path, target_path, file_size, total_chunks,
        )

        # Phase 1: Send start
        await self._send({
            "type": "upload_file_start",
            "request_id": request_id,
            "executor_id": self._executor_id,
            "target_path": target_path,
            "total_chunks": total_chunks,
            "total_size_bytes": file_size,
            "content_type": _guess_content_type(local_path),
        })

        # Phase 2: Stream chunks
        sha256 = hashlib.sha256()
        chunk_index = 0

        try:
            with open(local_path, "rb") as fh:
                while True:
                    raw = fh.read(CHUNK_SIZE_RAW)
                    if not raw:
                        break
                    sha256.update(raw)
                    data_b64 = base64.b64encode(raw).decode("ascii")

                    await self._send({
                        "type": "upload_file_chunk",
                        "request_id": request_id,
                        "executor_id": self._executor_id,
                        "chunk_index": chunk_index,
                        "data_b64": data_b64,
                    })
                    chunk_index += 1

        except OSError as e:
            return {"ok": False, "error": f"Read error: {e}"}

        # Phase 3: Send complete with checksum
        checksum = f"sha256:{sha256.hexdigest()}"

        await self._send({
            "type": "upload_file_complete",
            "request_id": request_id,
            "executor_id": self._executor_id,
            "total_chunks": chunk_index,
            "total_size_bytes": file_size,
            "checksum": checksum,
        })

        logger.info(
            "Upload complete: target=%s size=%d chunks=%d checksum=%s",
            target_path, file_size, chunk_index, checksum,
        )

        return {
            "ok": True,
            "target_path": target_path,
            "size_bytes": file_size,
            "checksum": checksum,
        }


def _guess_content_type(path: str) -> str:
    """Simple content type detection from file extension."""
    ext = os.path.splitext(path)[1].lower()
    mapping = {
        ".json": "application/json",
        ".csv": "text/csv",
        ".txt": "text/plain",
        ".html": "text/html",
        ".xml": "application/xml",
        ".pdf": "application/pdf",
        ".png": "image/png",
        ".jpg": "image/jpeg",
        ".jpeg": "image/jpeg",
        ".zip": "application/zip",
        ".gz": "application/gzip",
        ".tar": "application/x-tar",
    }
    return mapping.get(ext, "application/octet-stream")
