"""
Storage helpers for local filesystem and S3-compatible backends (Cloudflare R2).
"""
from __future__ import annotations

import json
import os
import tempfile
from functools import lru_cache
from pathlib import Path
from typing import Any, Dict, List, Optional
from urllib.parse import urlparse

import boto3
from botocore.exceptions import ClientError


def _as_bool(value: Optional[str], default: bool = False) -> bool:
    if value is None:
        return default
    return value.strip().lower() in {"1", "true", "yes", "on"}


def use_local_storage() -> bool:
    """
    Local storage toggle.

    Defaults to False, so remote storage is used unless USE_LOCAL is explicitly true.
    """
    return _as_bool(os.getenv("USE_LOCAL"), default=False)


def _normalize_path(path: str) -> str:
    normalized = path.strip()
    if normalized.startswith("./"):
        normalized = normalized[2:]
    normalized = normalized.replace("\\", "/")
    if normalized.startswith("/"):
        normalized = normalized[1:]
    return normalized


def _bucket() -> str:
    bucket = os.getenv("MEDIA_PRIVATE_BUCKET", "").strip()
    if not bucket:
        raise ValueError("MEDIA_PRIVATE_BUCKET is required when USE_LOCAL is false")
    return bucket


def _region() -> str:
    region = os.getenv("MEDIA_REGION", "").strip()
    if not region:
        raise ValueError("MEDIA_REGION is required when USE_LOCAL is false")
    return region


def _endpoint_url() -> str:
    endpoint = os.getenv("MEDIA_ENDPOINT_URL", "").strip()
    if not endpoint:
        raise ValueError("MEDIA_ENDPOINT_URL is required when USE_LOCAL is false")
    return endpoint


def _access_key() -> str:
    access_key = os.getenv("MEDIA_ACCESS_KEY_ID", "").strip()
    if not access_key:
        raise ValueError("MEDIA_ACCESS_KEY_ID is required when USE_LOCAL is false")
    return access_key


def _secret_key() -> str:
    secret_key = os.getenv("MEDIA_SECRET_ACCESS_KEY", "").strip()
    if not secret_key:
        raise ValueError("MEDIA_SECRET_ACCESS_KEY is required when USE_LOCAL is false")
    return secret_key


@lru_cache(maxsize=1)
def _client():
    return boto3.client(
        "s3",
        region_name=_region(),
        endpoint_url=_endpoint_url(),
        aws_access_key_id=_access_key(),
        aws_secret_access_key=_secret_key(),
    )


def uri_from_key(key: str) -> str:
    return f"s3://{_bucket()}/{key}"


def key_from_path(path_or_uri: str) -> str:
    value = path_or_uri.strip()
    if value.startswith("s3://"):
        parsed = urlparse(value)
        bucket = parsed.netloc
        key = parsed.path.lstrip("/")
        expected = _bucket()
        if bucket != expected:
            raise ValueError(f"Bucket mismatch: got '{bucket}', expected '{expected}'")
        if not key:
            raise ValueError("Invalid S3 URI: missing object key")
        return key
    return _normalize_path(value)


def ensure_parent_dir(path: str) -> None:
    Path(path).parent.mkdir(parents=True, exist_ok=True)


def path_exists(path_or_uri: str) -> bool:
    if os.path.exists(path_or_uri):
        return True

    is_s3_uri = path_or_uri.strip().startswith("s3://")
    if use_local_storage() and not is_s3_uri:
        return False

    try:
        key = key_from_path(path_or_uri)
    except Exception:
        return False

    try:
        _client().head_object(Bucket=_bucket(), Key=key)
        return True
    except ClientError as exc:
        status = exc.response.get("ResponseMetadata", {}).get("HTTPStatusCode")
        if status == 404:
            return False
        error_code = exc.response.get("Error", {}).get("Code")
        if error_code in {"404", "NoSuchKey", "NotFound"}:
            return False
        raise


def write_text(path_or_uri: str, content: str, content_type: str = "text/plain; charset=utf-8") -> str:
    if use_local_storage():
        ensure_parent_dir(path_or_uri)
        with open(path_or_uri, "w", encoding="utf-8") as f:
            f.write(content)
        return path_or_uri

    key = key_from_path(path_or_uri)
    _client().put_object(
        Bucket=_bucket(),
        Key=key,
        Body=content.encode("utf-8"),
        ContentType=content_type,
    )
    return uri_from_key(key)


def write_json(path_or_uri: str, data: Any) -> str:
    content = json.dumps(data, indent=2, ensure_ascii=False)
    return write_text(path_or_uri, content, content_type="application/json; charset=utf-8")


def write_bytes(path_or_uri: str, content: bytes, content_type: str = "application/octet-stream") -> str:
    if use_local_storage():
        ensure_parent_dir(path_or_uri)
        with open(path_or_uri, "wb") as f:
            f.write(content)
        return path_or_uri

    key = key_from_path(path_or_uri)
    _client().put_object(Bucket=_bucket(), Key=key, Body=content, ContentType=content_type)
    return uri_from_key(key)


def read_text(path_or_uri: str) -> str:
    if os.path.exists(path_or_uri):
        with open(path_or_uri, "r", encoding="utf-8") as f:
            return f.read()

    is_s3_uri = path_or_uri.strip().startswith("s3://")
    if use_local_storage() and not is_s3_uri:
        with open(path_or_uri, "r", encoding="utf-8") as f:
            return f.read()

    key = key_from_path(path_or_uri)
    response = _client().get_object(Bucket=_bucket(), Key=key)
    body = response["Body"].read()
    return body.decode("utf-8")


def read_bytes(path_or_uri: str) -> bytes:
    if os.path.exists(path_or_uri):
        with open(path_or_uri, "rb") as f:
            return f.read()

    is_s3_uri = path_or_uri.strip().startswith("s3://")
    if use_local_storage() and not is_s3_uri:
        with open(path_or_uri, "rb") as f:
            return f.read()

    key = key_from_path(path_or_uri)
    response = _client().get_object(Bucket=_bucket(), Key=key)
    return response["Body"].read()


def read_json(path_or_uri: str) -> Any:
    return json.loads(read_text(path_or_uri))


def materialize_to_local_file(path_or_uri: str) -> str:
    """
    Ensure a path-like reference is available as a local file path.
    Returns the original path when it is already local.
    """
    if os.path.exists(path_or_uri):
        return str(Path(path_or_uri).expanduser().resolve())

    blob = read_bytes(path_or_uri)
    suffix = Path(path_or_uri).suffix or ""
    tmp_file = tempfile.NamedTemporaryFile(delete=False, suffix=suffix)
    with tmp_file:
        tmp_file.write(blob)
    return tmp_file.name


def copy_path(source_path_or_uri: str, destination_path_or_uri: str) -> str:
    if use_local_storage():
        ensure_parent_dir(destination_path_or_uri)
        import shutil

        shutil.copy2(source_path_or_uri, destination_path_or_uri)
        return destination_path_or_uri

    source_key = key_from_path(source_path_or_uri)
    destination_key = key_from_path(destination_path_or_uri)
    _client().copy_object(
        Bucket=_bucket(),
        Key=destination_key,
        CopySource={"Bucket": _bucket(), "Key": source_key},
    )
    return uri_from_key(destination_key)


def list_objects(prefix: str) -> List[Dict[str, Any]]:
    """
    List objects for a prefix. Prefix is interpreted as a logical output path.
    """
    if use_local_storage():
        normalized_prefix = _normalize_path(prefix)
        root = Path(normalized_prefix)
        if not root.exists():
            return []
        results: List[Dict[str, Any]] = []
        for file_path in root.rglob("*"):
            if file_path.is_file():
                results.append(
                    {
                        "key": _normalize_path(str(file_path)),
                        "last_modified": file_path.stat().st_mtime,
                    }
                )
        return results

    normalized_prefix = key_from_path(prefix)
    paginator = _client().get_paginator("list_objects_v2")
    results: List[Dict[str, Any]] = []

    for page in paginator.paginate(Bucket=_bucket(), Prefix=normalized_prefix):
        for obj in page.get("Contents", []):
            results.append(
                {
                    "key": obj["Key"],
                    "last_modified": obj["LastModified"].timestamp(),
                }
            )

    return results


def remove_path(path_or_uri: str) -> None:
    if use_local_storage():
        if os.path.exists(path_or_uri):
            os.remove(path_or_uri)
        return

    key = key_from_path(path_or_uri)
    _client().delete_object(Bucket=_bucket(), Key=key)
