import xml.etree.ElementTree as ET
from pathlib import Path
from typing import Dict, Sequence, Tuple
from urllib.parse import quote

import requests
from flask import Flask

from . import utils
from .ks3_auth import add_auth_header
from .progress import ProgressFile
from .types import CompletedPart


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 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) -> str:
        return self.client.abort_multipart_upload(object_key, upload_id)

    def list_multipart_uploads(self) -> str:
        return self.client.list_multipart_uploads()

    def list_parts(self, object_key: str, upload_id: str) -> str:
        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)


class KS3Client:
    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}"

    def get_url_and_headers(
        self,
        method: str,
        object_key: str,
        query: str = "",
        headers: Dict[str, str] | None = None,
    ) -> Tuple[str, Dict[str, str]]:
        if not all([self.access_key, self.secret, self.endpoint, self.bucket_name]):
            raise RuntimeError("FlaskKS3 is not initialized; call init_app first")

        headers = dict(headers) if headers else {}
        add_auth_header(
            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 put_object(self, object_key: str, local_path: Path) -> requests.Response:
        url, headers = self.get_url_and_headers("PUT", object_key)
        with local_path.open("rb") as f:
            total_size = local_path.stat().st_size
            progress_file = ProgressFile(f, total_size)
            response = requests.put(
                url, headers=headers, data=progress_file, timeout=3600
            )
        if response.status_code == 200:
            print("普通上传成功")
        else:
            print(f"上传失败: {response.status_code}, {response.text}")
        return response

    def head_object(self, object_key: str) -> requests.Response:
        url, headers = self.get_url_and_headers("HEAD", object_key)
        response = requests.request("HEAD", url, headers=headers, timeout=60)
        if response.status_code == 200:
            print(f"xyyebesr 文件已存在 {object_key}")
        elif response.status_code == 404:
            print(f"b1s6sstz 文件不存在 {object_key}")
        else:
            print(
                f"20z4ojsw 文件状态码错误 {object_key}, {response.status_code}, {response.text}"
            )
        return response

    # 下面的方法是分快上传相关的方法 https://docs.ksyun.com/documents/42466?type=3
    def initiate_multipart_upload(self, object_key: str) -> str:
        """获取分块上传的upload_id"""
        url, headers = self.get_url_and_headers("POST", object_key, query="uploads")
        response = requests.post(url, headers=headers)
        if response.status_code == 200:
            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)
            print(f"UploadId: {upload_id}")
            if upload_id:
                return upload_id
            raise RuntimeError("未从响应中解析到 UploadId")

        raise RuntimeError(
            f"分块初始化上传失败 initiate_upload: {response.status_code}, {response.text}"
        )

    def abort_multipart_upload(self, object_key: str, upload_id: str) -> None:
        url, headers = self.get_url_and_headers(
            "DELETE", object_key, query=f"uploadId={upload_id}"
        )
        response = requests.delete(url, headers=headers)
        assert (
            response.status_code == 204
        ), f"中止分块上传失败 abort_upload: {response.status_code}, {response.text}"
        return response

    def list_multipart_uploads(self) -> requests.Response:
        url, headers = self.get_url_and_headers("GET", "", query="uploads")
        response = requests.get(url, headers=headers)
        assert (
            response.status_code == 200
        ), f"列出分块上传失败 list_uploads: {response.status_code}, {response.text}"
        return response

    def list_parts(self, object_key: str, upload_id: str) -> requests.Response:
        url, headers = self.get_url_and_headers(
            "GET", object_key, query=f"uploadId={upload_id}"
        )
        response = requests.get(url, headers=headers)
        assert (
            response.status_code == 200
        ), f"列出分块失败 list_parts: {response.status_code}, {response.text}"
        return response

    def upload_part(
        self,
        object_key: str,
        upload_id: str,
        part_number: int,
        filepath: str | Path,
        filesize: int = 0,
    ) -> str:
        """上传单个分片并返回 ETag。"""
        query = f"partNumber={part_number}&uploadId={upload_id}"
        url, headers = self.get_url_and_headers("PUT", object_key, query=query)
        with Path(filepath).open("rb") as f:
            total_size = filesize or Path(filepath).stat().st_size
            progress_file = ProgressFile(f, total_size)
            response = requests.put(
                url, headers=headers, data=progress_file, timeout=120
            )
            print(
                f"上传分片 {part_number} , 状态码: {response.status_code}, text: {response.text}"
            )
        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")
        clean_etag = etag.strip('"')
        print(f"分片 {part_number} 上传成功，ETag: {clean_etag}")
        return clean_etag

    def complete_multipart_upload(
        self, object_key: str, upload_id: str, parts: Sequence[CompletedPart]
    ) -> requests.Response:
        xml_body = utils.generate_complete_multipart_xml(parts)
        query = f"uploadId={upload_id}"
        base_headers = {"Content-Type": "application/xml"}
        url, headers = self.get_url_and_headers(
            "POST", object_key, query=query, headers=base_headers
        )
        print(f"url: {url}, headers: {headers}")
        print(f"xml_body: {xml_body}")

        response = requests.post(url, headers=headers, data=xml_body)
        if response.status_code != 200:
            raise RuntimeError(f"合并失败: {response.status_code}, {response.text}")
        print("所有分片合并成功，上传完成！")
        return response
