import logging
import xml.etree.ElementTree as ET
from contextlib import contextmanager
from pathlib import Path
from time import time
from typing import Any, Dict, Sequence
from urllib.parse import quote, urlencode

import requests
from flask import Flask

from . import utils
from .ks3_auth import add_auth_header as add_auth_header_v3
from .ks3_auth import canonical_string as canonical_string_v3
from .ks3_auth import encode as encode_v3
from .progress import ProgressFile
from .types import CompletedPart

logger = logging.getLogger(__name__)


class FlaskKS3:
    def __init__(self, app=None):
        if app is not None:
            self.init_app(app)

    def init_app(self, app: Flask):
        self.access_key = app.config["KS3_ACCESS_KEY"]
        self.secret = app.config["KS3_SECRET"]
        self.endpoint = app.config["KS3_ENDPOINT"]
        self.bucket_name = app.config["KS3_BUCKET"]

        self.client = KS3Client(
            access_key=self.access_key,
            secret=self.secret,
            bucket_name=self.bucket_name,
            endpoint=self.endpoint,
        )

    def put_object(self, object_key: str, local_path: Path) -> requests.Response:
        return self.client.put_object(object_key, local_path)

    def put_object_copy(
        self,
        object_key: str,
        source_object_key: str,
        source_bucket_name: str | None = None,
        headers: Dict[str, str] | None = None,
    ) -> requests.Response:
        return self.client.put_object_copy(
            object_key,
            source_object_key,
            source_bucket_name=source_bucket_name,
            headers=headers,
        )

    def get_object(self, object_key: str, **kwargs: Any) -> requests.Response:
        return self.client.get_object(object_key, **kwargs)

    def head_object(self, object_key: str) -> requests.Response:
        return self.client.head_object(object_key)

    def initiate_multipart_upload(self, object_key: str) -> str:
        return self.client.initiate_multipart_upload(object_key)

    def abort_multipart_upload(
        self, object_key: str, upload_id: str
    ) -> requests.Response:
        return self.client.abort_multipart_upload(object_key, upload_id)

    def list_multipart_uploads(self) -> requests.Response:
        return self.client.list_multipart_uploads()

    def list_parts(self, object_key: str, upload_id: str) -> requests.Response:
        return self.client.list_parts(object_key, upload_id)

    def upload_part(
        self, object_key: str, upload_id: str, part_number: int, filepath: str | Path
    ) -> str:
        return self.client.upload_part(object_key, upload_id, part_number, filepath)

    def complete_multipart_upload(
        self, object_key: str, upload_id: str, parts: Sequence[CompletedPart]
    ) -> requests.Response:
        return self.client.complete_multipart_upload(object_key, upload_id, parts)

    def list_objects(
        self,
        prefix: str | None = None,
        marker: str | None = None,
        max_keys: int | None = None,
        delimiter: str | None = None,
    ) -> requests.Response:
        return self.client.list_objects(prefix, marker, max_keys, delimiter)

    def generate_share_url(
        self,
        object_key: str,
        expires_in: int = 3600,
        response_headers: Dict[str, str] | None = None,
    ) -> str:
        return self.client.generate_share_url(
            object_key,
            expires_in=expires_in,
            response_headers=response_headers,
        )


class KS3Client:
    DEFAULT_TIMEOUT = (30, 1800)

    def __init__(
        self,
        access_key: str,
        secret: str,
        bucket_name: str,
        endpoint: str,
    ):
        self.access_key = access_key
        self.secret = secret
        self.bucket_name = bucket_name
        self.endpoint = endpoint
        self._base_url = f"https://{self.bucket_name}.{self.endpoint}"

    @contextmanager
    def session_client(self):
        with requests.Session() as session:
            yield SessionKS3Client(self, session)

    def _ensure_initialized(self) -> None:
        if not all([self.access_key, self.secret, self.endpoint, self.bucket_name]):
            raise RuntimeError("FlaskKS3 is not initialized; call init_app first")

    def _request(
        self,
        method: str,
        object_key: str,
        query: str = "",
        headers: Dict[str, str] | None = None,
        session: requests.Session | None = None,
        **kwargs: Any,
    ) -> requests.Response:
        url, signed_headers = self.get_url_and_headers(
            method,
            object_key,
            query=query,
            headers=headers,
        )
        kwargs.setdefault("timeout", self.DEFAULT_TIMEOUT)
        requester = session.request if session else requests.request
        return requester(method, url, headers=signed_headers, **kwargs)

    @staticmethod
    def _ensure_status(
        response: requests.Response,
        expected_statuses: int | Sequence[int],
        action: str,
    ) -> requests.Response:
        expected = (
            {expected_statuses}
            if isinstance(expected_statuses, int)
            else set(expected_statuses)
        )
        if response.status_code not in expected:
            raise RuntimeError(
                f"{action} failed: {response.status_code}, {response.text}"
            )
        return response

    def get_url_and_headers(
        self,
        method: str,
        object_key: str,
        query: str = "",
        headers: Dict[str, str] | None = None,
    ) -> tuple[str, Dict[str, str]]:
        self._ensure_initialized()

        headers = dict(headers) if headers else {}
        add_auth_header_v3(
            self.access_key,
            self.secret,
            headers,
            method,
            self.bucket_name,
            object_key,
            query,
        )
        quoted_key = quote(object_key, safe="/")
        url = f"{self._base_url}/{quoted_key}"
        if query:
            url = f"{url}?{query}"
        return url, headers

    def generate_share_url(
        self,
        object_key: str,
        expires_in: int = 3600,
        response_headers: Dict[str, str] | None = None,
    ) -> str:
        self._ensure_initialized()
        if expires_in <= 0:
            raise ValueError("expires_in 必须大于 0")
        normalized_key = object_key.lstrip("/")
        if not normalized_key:
            raise ValueError("object_key 不能为空")

        expires = int(time()) + expires_in
        query_args = {
            "KSSAccessKeyId": self.access_key,
            "Expires": str(expires),
        }
        if response_headers:
            query_args.update(
                {
                    key: value
                    for key, value in response_headers.items()
                    if value is not None
                }
            )

        canonical_query = urlencode(query_args)
        signature = encode_v3(
            self.secret,
            canonical_string_v3(
                "GET",
                self.bucket_name,
                normalized_key,
                canonical_query,
                headers={},
                expires=expires,
            ),
        )
        query_args["Signature"] = signature

        quoted_key = quote(normalized_key, safe="/")
        return f"{self._base_url}/{quoted_key}?{urlencode(query_args)}"

    def put_object(self, object_key: str, local_path: Path) -> requests.Response:
        with ProgressFile(local_path) as progress_file:
            response = self._request("PUT", object_key, data=progress_file)
        if response.status_code == 200:
            logger.info("Upload completed for object '%s'", object_key)
        else:
            logger.warning(
                "Upload failed for object '%s': %s %s",
                object_key,
                response.status_code,
                response.text,
            )
        return response

    def put_object_copy(
        self,
        object_key: str,
        source_object_key: str,
        session: requests.Session | None = None,
        source_bucket_name: str | None = None,
        headers: Dict[str, str] | None = None,
    ) -> requests.Response:
        source_bucket = source_bucket_name or self.bucket_name
        normalized_source_key = source_object_key.lstrip("/")
        if not normalized_source_key:
            raise ValueError("source_object_key 不能为空")

        request_headers = dict(headers) if headers else {}
        request_headers["x-kss-copy-source"] = (
            f"/{source_bucket}/{quote(normalized_source_key, safe='/')}"
        )

        response = self._request(
            "PUT", object_key, headers=request_headers, session=session
        )
        if response.status_code == 200:
            logger.info(
                "Object copied from '%s/%s' to '%s'",
                source_bucket,
                normalized_source_key,
                object_key,
            )
        else:
            logger.warning(
                "Copy failed from '%s/%s' to '%s': %s %s",
                source_bucket,
                normalized_source_key,
                object_key,
                response.status_code,
                response.text,
            )
        return response

    def get_object(
        self,
        object_key: str,
        session: requests.Session | None = None,
        **kwargs: Any,
    ) -> requests.Response:
        request_headers = kwargs.pop("headers", None)
        response = self._request(
            "GET",
            object_key,
            headers=request_headers,
            session=session,
            **kwargs,
        )
        if response.status_code in (200, 206):
            logger.info("Object is available: %s", object_key)
        elif response.status_code == 404:
            logger.info("Object not found: %s", object_key)
        else:
            logger.warning(
                "Unexpected status for object '%s': %s %s",
                object_key,
                response.status_code,
                response.text,
            )
        return response

    def head_object(self, object_key: str) -> requests.Response:
        response = self._request("HEAD", object_key)
        if response.status_code == 200:
            logger.info("Object is available: %s", object_key)
        elif response.status_code == 404:
            logger.info("Object not found: %s", object_key)
        else:
            logger.warning(
                "Unexpected status for object '%s': %s %s",
                object_key,
                response.status_code,
                response.text,
            )
        return response

    def initiate_multipart_upload(self, object_key: str) -> str:
        response = self._request("POST", object_key, query="uploads")
        self._ensure_status(response, 200, "initiate multipart upload")

        xml_root = ET.fromstring(response.text)
        ns = {"s3": "http://s3.amazonaws.com/doc/2006-03-01/"}
        upload_id = xml_root.findtext("s3:UploadId", namespaces=ns)
        if upload_id:
            logger.info("Multipart upload initiated for '%s'", object_key)
            return upload_id
        raise RuntimeError("未从响应中解析到 UploadId")

    def abort_multipart_upload(
        self, object_key: str, upload_id: str
    ) -> requests.Response:
        response = self._request(
            "DELETE",
            object_key,
            query=f"uploadId={upload_id}",
        )
        return self._ensure_status(response, 204, "abort multipart upload")

    def list_multipart_uploads(self) -> requests.Response:
        response = self._request("GET", "", query="uploads")
        return self._ensure_status(response, 200, "list multipart uploads")

    def list_parts(self, object_key: str, upload_id: str) -> requests.Response:
        response = self._request("GET", object_key, query=f"uploadId={upload_id}")
        return self._ensure_status(response, 200, "list multipart parts")

    def upload_part(
        self,
        object_key: str,
        upload_id: str,
        part_number: int,
        filepath: str | Path,
        filesize: int = 0,
    ) -> str:
        del filesize
        query = f"partNumber={part_number}&uploadId={upload_id}"
        with ProgressFile(filepath) as progress_file:
            response = self._request("PUT", object_key, query=query, data=progress_file)
        if response.status_code != 200:
            raise RuntimeError(
                f"分片 {part_number} 上传失败: {response.status_code}, {response.text}"
            )

        etag = response.headers.get("ETag")
        if not etag:
            raise RuntimeError(f"分片 {part_number} 上传成功但缺少 ETag")
        logger.info("Multipart part %s uploaded for '%s'", part_number, object_key)
        return etag.strip('"')

    def complete_multipart_upload(
        self, object_key: str, upload_id: str, parts: Sequence[CompletedPart]
    ) -> requests.Response:
        xml_body = utils.generate_complete_multipart_xml(parts)
        response = self._request(
            "POST",
            object_key,
            query=f"uploadId={upload_id}",
            headers={"Content-Type": "application/xml"},
            data=xml_body,
        )
        if response.status_code != 200:
            raise RuntimeError(f"合并失败: {response.status_code}, {response.text}")
        logger.info("Multipart upload completed for '%s'", object_key)
        return response

    def list_objects(
        self,
        prefix: str | None = None,
        marker: str | None = None,
        max_keys: int | None = None,
        delimiter: str | None = None,
    ) -> requests.Response:
        query = {}
        if prefix:
            query["prefix"] = prefix
        if marker:
            query["marker"] = marker
        if max_keys is not None:
            query["max-keys"] = str(max_keys)
        if delimiter:
            query["delimiter"] = delimiter
        response = self._request("GET", "", query=urlencode(query))
        return self._ensure_status(response, 200, "list objects")


class SessionKS3Client:
    def __init__(self, client: KS3Client, session: requests.Session):
        self._client = client
        self._session = session

    def __getattr__(self, item: str) -> Any:
        return getattr(self._client, item)

    def get_object(self, object_key: str, **kwargs: Any) -> requests.Response:
        kwargs.setdefault("session", self._session)
        return self._client.get_object(object_key, **kwargs)
