from __future__ import annotations

import argparse
import re
import sys
import zipfile
from dataclasses import dataclass, replace
from datetime import date
from pathlib import Path
from typing import Any, Iterable

from app.utils.stt_audio_upload import OCIUploadConfig, _oci_client, _oci_upload_config_from_env


_SAFE_SESSION_ID = re.compile(r"^[A-Za-z0-9_.-]+$")


@dataclass(frozen=True, slots=True)
class DownloadResult:
    prefix: str
    output_dir: Path
    object_count: int
    downloaded_count: int
    cached_count: int
    total_bytes: int
    zip_path: Path | None


def _parse_date(value: str) -> str:
    try:
        return date.fromisoformat(value).isoformat()
    except ValueError as exc:
        raise argparse.ArgumentTypeError("use uma data no formato AAAA-MM-DD") from exc


def _parse_session_id(value: str) -> str:
    session_id = value.strip()
    if not session_id or not _SAFE_SESSION_ID.fullmatch(session_id):
        raise argparse.ArgumentTypeError(
            "session-id deve conter apenas letras, numeros, ponto, hifen ou underscore"
        )
    return session_id


def segments_prefix(day: str, session_id: str) -> str:
    return f"{day}/{session_id}/"


def entire_calls_prefix(day: str) -> str:
    return f"{day}/entire_call/"


def _list_objects(client: Any, config: OCIUploadConfig, prefix: str) -> list[Any]:
    objects: list[Any] = []
    start: str | None = None
    while True:
        response = client.list_objects(
            namespace_name=config.namespace,
            bucket_name=config.bucket,
            prefix=prefix,
            start=start,
            fields="name,size,etag",
        )
        objects.extend(
            item
            for item in response.data.objects
            if getattr(item, "name", "") and not item.name.endswith("/")
        )
        start = getattr(response.data, "next_start_with", None)
        if not start:
            return objects


def _safe_relative_name(object_name: str, prefix: str) -> Path:
    if not object_name.startswith(prefix):
        raise ValueError(f"objeto fora do prefixo solicitado: {object_name}")
    relative = Path(object_name[len(prefix) :])
    if not relative.parts or relative.is_absolute() or ".." in relative.parts:
        raise ValueError(f"nome de objeto inseguro: {object_name}")
    return relative


def _response_chunks(response: Any) -> Iterable[bytes]:
    raw = getattr(getattr(response, "data", None), "raw", None)
    if raw is not None and hasattr(raw, "stream"):
        yield from raw.stream(1024 * 1024, decode_content=False)
        return
    data = getattr(response, "data", None)
    content = getattr(data, "content", data)
    if isinstance(content, bytes):
        yield content
        return
    raise TypeError("resposta do Object Storage nao contem um corpo legivel")


def _create_zip(output_dir: Path, zip_path: Path) -> None:
    zip_path.parent.mkdir(parents=True, exist_ok=True)
    temporary = zip_path.with_suffix(zip_path.suffix + ".part")
    try:
        with zipfile.ZipFile(temporary, "w", compression=zipfile.ZIP_DEFLATED) as archive:
            for path in sorted(output_dir.rglob("*")):
                if path.is_file() and not path.name.endswith(".part"):
                    archive.write(path, path.relative_to(output_dir))
        temporary.replace(zip_path)
    finally:
        temporary.unlink(missing_ok=True)


def download_prefix(
    *,
    client: Any,
    config: OCIUploadConfig,
    prefix: str,
    output_dir: Path,
    zip_path: Path | None,
) -> DownloadResult:
    objects = _list_objects(client, config, prefix)
    if not objects:
        raise FileNotFoundError(
            f"nenhum objeto encontrado em bucket={config.bucket} prefix={prefix}"
        )

    output_dir.mkdir(parents=True, exist_ok=True)
    downloaded = cached = total_bytes = 0
    print(f"bucket={config.bucket} prefix={prefix} objetos={len(objects)}", flush=True)

    for index, item in enumerate(objects, 1):
        destination = output_dir / _safe_relative_name(item.name, prefix)
        destination.parent.mkdir(parents=True, exist_ok=True)
        expected_size = int(getattr(item, "size", 0) or 0)
        if destination.exists() and expected_size and destination.stat().st_size == expected_size:
            cached += 1
            total_bytes += expected_size
            print(f"[{index}/{len(objects)}] cache {item.name}", flush=True)
            continue

        temporary = destination.with_suffix(destination.suffix + ".part")
        try:
            response = client.get_object(
                namespace_name=config.namespace,
                bucket_name=config.bucket,
                object_name=item.name,
            )
            with temporary.open("wb") as handle:
                for chunk in _response_chunks(response):
                    handle.write(chunk)
            actual_size = temporary.stat().st_size
            if expected_size and actual_size != expected_size:
                raise IOError(
                    f"tamanho divergente para {item.name}: "
                    f"esperado={expected_size} recebido={actual_size}"
                )
            temporary.replace(destination)
        finally:
            temporary.unlink(missing_ok=True)

        downloaded += 1
        total_bytes += destination.stat().st_size
        print(f"[{index}/{len(objects)}] baixado {item.name}", flush=True)

    if zip_path is not None:
        _create_zip(output_dir, zip_path)
        print(f"zip={zip_path} bytes={zip_path.stat().st_size}", flush=True)

    return DownloadResult(
        prefix, output_dir, len(objects), downloaded, cached, total_bytes, zip_path
    )


def _parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        description="Baixa audios do OCI Object Storage usados pelo TIA."
    )
    parser.add_argument("--bucket", help="sobrescreve BUCKET_NAME (dev, fqa ou prd)")
    parser.add_argument(
        "--output-root",
        type=Path,
        default=Path("recordings/object_storage"),
        help="diretorio raiz (padrao: recordings/object_storage)",
    )
    parser.add_argument("--no-zip", action="store_true", help="nao gera o ZIP final")
    commands = parser.add_subparsers(dest="command", required=True)
    segments = commands.add_parser("segments", help="segmentos de uma chamada")
    segments.add_argument("--date", required=True, type=_parse_date)
    segments.add_argument("--session-id", required=True, type=_parse_session_id)
    entire = commands.add_parser("entire-calls", help="chamadas completas de um dia")
    entire.add_argument("--date", required=True, type=_parse_date)
    return parser


def main(argv: list[str] | None = None) -> int:
    args = _parser().parse_args(argv)
    config = _oci_upload_config_from_env()
    if config is None:
        print("erro: configuracao OCI BUCKET_* incompleta", file=sys.stderr)
        return 2
    if args.bucket:
        config = replace(config, bucket=args.bucket.strip())

    if args.command == "segments":
        prefix = segments_prefix(args.date, args.session_id)
        output_dir = args.output_root / args.date / "segments" / args.session_id
        zip_path = None if args.no_zip else args.output_root / f"{args.date}_{args.session_id}_segments.zip"
    else:
        prefix = entire_calls_prefix(args.date)
        output_dir = args.output_root / args.date / "entire_call"
        zip_path = None if args.no_zip else args.output_root / f"{args.date}_entire_calls.zip"

    try:
        result = download_prefix(
            client=_oci_client(config),
            config=config,
            prefix=prefix,
            output_dir=output_dir,
            zip_path=zip_path,
        )
    except Exception as exc:
        print(f"erro: {exc}", file=sys.stderr)
        return 1
    print(
        f"concluido objetos={result.object_count} baixados={result.downloaded_count} "
        f"cache={result.cached_count} bytes={result.total_bytes}",
        flush=True,
    )
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
