"""boto3 client construction and the error types the rest of the package raises.

Credentials are never hardcoded. boto3's standard provider chain is used, which
reads, in order: AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY (+ optional
AWS_SESSION_TOKEN) from the environment, then ~/.aws/credentials (honouring
AWS_PROFILE), then instance/container roles.
"""

from __future__ import annotations

import boto3
from botocore.config import Config
from botocore.exceptions import (
    BotoCoreError,
    ClientError,
    NoCredentialsError,
    PartialCredentialsError,
)

from s3_config import bucket_name, region_name


# --- Errors -----------------------------------------------------------------


class S3IntegrationError(Exception):
    """Base class for every error this package raises."""


class MissingCredentialsError(S3IntegrationError):
    """No usable AWS credentials were found."""


class BucketNotFoundError(S3IntegrationError):
    """The bucket does not exist, or this identity may not see it."""


class UploadFailedError(S3IntegrationError):
    """An upload did not complete."""


class ObjectNotFoundError(S3IntegrationError):
    """The requested key does not exist in the bucket."""


class ClassificationUnclearError(S3IntegrationError):
    """A file's folder could not be determined confidently and nobody confirmed it."""


_CREDENTIAL_HELP = (
    "No AWS credentials found. Set AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY and "
    "AWS_DEFAULT_REGION in the environment (see .env.example), or configure "
    "~/.aws/credentials with `aws configure`."
)


# --- Client -----------------------------------------------------------------


def get_s3_client(region: str | None = None):
    """Return a configured S3 client.

    Raises MissingCredentialsError when the provider chain yields nothing, so the
    failure surfaces here rather than as an opaque error on the first API call.
    """
    session = boto3.session.Session(region_name=region or region_name())

    try:
        credentials = session.get_credentials()
    except (BotoCoreError, ClientError) as exc:  # pragma: no cover - env dependent
        raise MissingCredentialsError(f"{_CREDENTIAL_HELP} (underlying error: {exc})") from exc

    if credentials is None:
        raise MissingCredentialsError(_CREDENTIAL_HELP)

    return session.client(
        "s3",
        config=Config(
            signature_version="s3v4",
            retries={"max_attempts": 3, "mode": "standard"},
        ),
    )


def verify_bucket(client=None, bucket: str | None = None) -> str:
    """Confirm the bucket exists and is reachable. Returns the bucket name."""
    client = client or get_s3_client()
    bucket = bucket or bucket_name()

    try:
        client.head_bucket(Bucket=bucket)
    except (NoCredentialsError, PartialCredentialsError) as exc:
        raise MissingCredentialsError(_CREDENTIAL_HELP) from exc
    except ClientError as exc:
        raise _translate_bucket_error(exc, bucket) from exc
    except BotoCoreError as exc:
        raise S3IntegrationError(f"Could not reach bucket '{bucket}': {exc}") from exc

    return bucket


def _translate_bucket_error(exc: ClientError, bucket: str) -> S3IntegrationError:
    code = str(exc.response.get("Error", {}).get("Code", ""))
    status = exc.response.get("ResponseMetadata", {}).get("HTTPStatusCode")

    if code in {"404", "NoSuchBucket"} or status == 404:
        return BucketNotFoundError(
            f"Bucket '{bucket}' does not exist in region '{region_name()}'. "
            f"Check S3_BUCKET_NAME / AWS_DEFAULT_REGION."
        )
    if code in {"403", "AccessDenied"} or status == 403:
        return BucketNotFoundError(
            f"Access denied to bucket '{bucket}'. The credentials in use are valid "
            f"but lack s3:ListBucket / s3:GetObject / s3:PutObject on it."
        )
    if code in {"401", "InvalidAccessKeyId", "SignatureDoesNotMatch"}:
        return MissingCredentialsError(
            f"AWS rejected the credentials in use ({code}). {_CREDENTIAL_HELP}"
        )
    return S3IntegrationError(f"Could not reach bucket '{bucket}': {exc}")
