"""ES 连接封装."""
from __future__ import annotations

import base64
import json
from typing import Optional

import urllib.request
from elasticsearch import Elasticsearch
from elasticsearch.exceptions import NotFoundError

from .models import (
    BulkResult,
    ConnectionConfig,
    EsVersionInfo,
    SafetyConfig,
    ScrollResult,
    SearchResult,
)


def get_es_version_by_http(url: str, username: str = "", password: str = "") -> Optional[EsVersionInfo]:
    """通过 HTTP 请求获取 ES 版本信息（不依赖 elasticsearch-py 客户端）.

    用于在创建客户端之前先获取版本，以便选择正确的客户端参数。
    """
    try:
        req = urllib.request.Request(url)
        if username and password:
            credentials = f"{username}:{password}".encode("ascii")
            encoded = base64.b64encode(credentials).decode("ascii")
            req.add_header("Authorization", f"Basic {encoded}")

        with urllib.request.urlopen(req, timeout=10) as response:
            data = json.loads(response.read().decode("utf-8"))
            version_str = data["version"]["number"]
            parts = version_str.split(".")
            return EsVersionInfo(
                major=int(parts[0]),
                minor=int(parts[1]),
                patch=int(parts[2].split("-")[0]) if "-" in parts[2] else int(parts[2]),
            )
    except Exception:
        return None


def create_client(config: ConnectionConfig, es_version: int = 7) -> Elasticsearch:
    """创建 ES 客户端.

    根据 ES 版本使用不同的参数：
    - ES 6/7: 使用 http_auth 参数
    - ES 8+: 使用 basic_auth 参数
    """
    kwargs = {
        "hosts": [config.url],
        "verify_certs": False,
    }

    if config.username and config.password:
        if es_version >= 8:
            kwargs["basic_auth"] = (config.username, config.password)
        else:
            kwargs["http_auth"] = (config.username, config.password)

    return Elasticsearch(**kwargs)


def get_es_version(client: Elasticsearch) -> EsVersionInfo:
    """获取 ES 版本信息（通过客户端）."""
    info = client.info()
    version_str = info["version"]["number"]
    parts = version_str.split(".")
    return EsVersionInfo(
        major=int(parts[0]),
        minor=int(parts[1]),
        patch=int(parts[2].split("-")[0]) if "-" in parts[2] else int(parts[2]),
    )


# ============== 统一操作接口 ==============

class ES6Ops:
    """ES 6 操作类（使用原生 HTTP，绕过 elasticsearch-py 兼容性问题）."""

    def __init__(self, url: str, username: str = "", password: str = ""):
        self.url = url.rstrip("/")
        self.username = username
        self.password = password

    def _make_request(self, path: str, method: str = "GET", body: Optional[dict] = None,
                     headers: Optional[dict] = None, content_type: str = "application/json") -> dict:
        """发送 HTTP 请求到 ES."""
        url = f"{self.url}{path}"
        req = urllib.request.Request(url, data=json.dumps(body).encode() if body else None, method=method)
        req.add_header("Content-Type", content_type)
        if headers:
            for k, v in headers.items():
                req.add_header(k, v)
        if self.username and self.password:
            credentials = f"{self.username}:{self.password}".encode("ascii")
            encoded = base64.b64encode(credentials).decode("ascii")
            req.add_header("Authorization", f"Basic {encoded}")

        try:
            with urllib.request.urlopen(req, timeout=30) as response:
                return json.loads(response.read().decode("utf-8"))
        except urllib.error.HTTPError as e:
            error_body = e.read().decode("utf-8") if e.fp else ""
            raise Exception(f"HTTP {e.code}: {error_body}")

    def ping(self) -> bool:
        """检查连接."""
        try:
            self._make_request("/")
            return True
        except Exception:
            return False

    def search(self, index: str, query: Optional[dict] = None, size: int = 10, from_: int = 0) -> SearchResult:
        """搜索文档."""
        body = query or {"query": {"match_all": {}}}
        # ES 6 中 _type 已废弃，直接删除
        if "_type" in body:
            del body["_type"]

        resp = self._make_request(f"/{index}/_search?size={size}&from={from_}", "POST", body)

        hits = [hit["_source"] for hit in resp["hits"]["hits"]]
        total = resp["hits"]["total"]
        if isinstance(total, dict):
            total = total["value"]

        return SearchResult(
            hits=hits,
            total=total,
            truncated=total > size,
        )

    def scroll(self, index: str, query: Optional[dict] = None, size: int = 100, scroll: str = "5m") -> ScrollResult:
        """滚动查询."""
        body = query or {"query": {"match_all": {}}}
        # ES 6 中 _type 已废弃，直接删除
        if "_type" in body:
            del body["_type"]

        resp = self._make_request(f"/{index}/_search?scroll={scroll}&size={size}", "POST", body)

        hits = [hit["_source"] for hit in resp["hits"]["hits"]]
        scroll_id = resp.get("_scroll_id")

        return ScrollResult(
            hits=hits,
            scroll_id=scroll_id,
            took_ms=resp.get("took", 0),
        )

    def scroll_next(self, scroll_id: str, scroll: str = "5m") -> ScrollResult:
        """滚动查询下一页."""
        body = {"scroll": scroll, "scroll_id": scroll_id}
        resp = self._make_request("/_search/scroll", "POST", body)

        hits = [hit["_source"] for hit in resp["hits"]["hits"]]
        new_scroll_id = resp.get("_scroll_id")

        return ScrollResult(
            hits=hits,
            scroll_id=new_scroll_id,
            took_ms=resp.get("took", 0),
        )

    def scroll_clear(self, scroll_id: str) -> None:
        """清除滚动上下文."""
        body = {"scroll_id": scroll_id}
        self._make_request("/_search/scroll", "DELETE", body)

    def index_doc(self, index: str, doc: dict, id_: Optional[str] = None) -> str:
        """索引文档."""
        if id_:
            resp = self._make_request(f"/{index}/_doc/{id_}", "PUT" if id_ else "POST", doc)
        else:
            resp = self._make_request(f"/{index}/_doc", "POST", doc)
        return resp["_id"]

    def update_doc(self, index: str, id_: str, doc: dict) -> None:
        """更新文档."""
        body = {"doc": doc}
        self._make_request(f"/{index}/_doc/{id_}/_update", "POST", body)

    def delete_doc(self, index: str, id_: str) -> None:
        """删除文档."""
        self._make_request(f"/{index}/_doc/{id_}", "DELETE")

    def bulk(self, operations: list[dict], refresh: bool = False) -> BulkResult:
        """批量操作（使用 ndjson 格式）."""
        # ES 6 bulk 格式：action 和 body 分行
        lines = []
        for op in operations:
            # action line
            action = {k: v for k, v in op.items() if not k.startswith("_")}
            index_name = op.get("_index")
            doc_type = op.get("_type", "_doc")
            doc_id = op.get("_id")

            action_line = {"index": {"_index": index_name, "_type": doc_type}}
            if doc_id:
                action_line["index"]["_id"] = doc_id
            lines.append(json.dumps(action_line, ensure_ascii=False))

            # body line (for index/create/update)
            if "index" in op or "create" in op or "update" in op:
                body = {k: v for k, v in op.items() if not k.startswith("_")}
                lines.append(json.dumps(body, ensure_ascii=False))

        body = "\n".join(lines) + "\n"
        url = "/_bulk"
        if refresh:
            url += "?refresh=true"

        req = urllib.request.Request(f"{self.url}{url}", data=body.encode(), method="POST")
        req.add_header("Content-Type", "application/x-ndjson")
        if self.username and self.password:
            credentials = f"{self.username}:{self.password}".encode("ascii")
            encoded = base64.b64encode(credentials).decode("ascii")
            req.add_header("Authorization", f"Basic {encoded}")

        with urllib.request.urlopen(req, timeout=30) as response:
            resp = json.loads(response.read().decode("utf-8"))

        succeeded = 0
        failed = 0
        errors = []

        for item in resp.get("items", []):
            action = item.get("index") or item.get("update") or item.get("delete") or item.get("create")
            if action.get("error"):
                failed += 1
                errors.append({
                    "action": list(item.keys())[0],
                    "id": action.get("_id"),
                    "error": action["error"].get("reason", "Unknown error"),
                })
            else:
                succeeded += 1

        return BulkResult(succeeded=succeeded, failed=failed, errors=errors)

    def list_indices(self) -> list[dict]:
        """列出所有索引."""
        resp = self._make_request("/_cat/indices?format=json")

        indices = []
        for idx in resp:
            name = idx.get("index") or idx.get("uuid")
            if not name:
                continue

            docs_count = idx.get("docs.count") or 0
            docs_count = int(docs_count) if docs_count else 0

            size = idx.get("store.size") or "0b"

            indices.append({
                "name": name,
                "docs_count": docs_count,
                "size": size,
                "health": idx.get("health", "unknown"),
            })
        return indices

    def get_index_mapping(self, index: str) -> dict:
        """获取索引 mapping."""
        return self._make_request(f"/{index}/_mapping")

    def create_index(self, index: str, mappings: Optional[dict] = None, settings: Optional[dict] = None) -> dict:
        """创建索引."""
        body = {}
        if mappings:
            body["mappings"] = mappings
        if settings:
            body["settings"] = settings
        return self._make_request(f"/{index}", "PUT", body if body else None)

    def delete_index(self, index: str) -> bool:
        """删除索引."""
        try:
            self._make_request(f"/{index}", "DELETE")
            return True
        except Exception as e:
            if "404" in str(e):
                return False
            raise

    def index_exists(self, index: str) -> bool:
        """检查索引是否存在."""
        try:
            req = urllib.request.Request(f"{self.url}/{index}", method="HEAD")
            if self.username and self.password:
                credentials = f"{self.username}:{self.password}".encode("ascii")
                encoded = base64.b64encode(credentials).decode("ascii")
                req.add_header("Authorization", f"Basic {encoded}")
            urllib.request.urlopen(req, timeout=10)
            return True
        except urllib.error.HTTPError as e:
            if e.code == 404:
                return False
            raise


class ES7Ops:
    """ES 7 操作类（使用 elasticsearch-py 7.x）."""

    def __init__(self, client: Elasticsearch):
        self.client = client

    def ping(self) -> bool:
        return self.client.ping()

    def search(self, index: str, query: Optional[dict] = None, size: int = 10, from_: int = 0) -> SearchResult:
        body = query or {"query": {"match_all": {}}}
        resp = self.client.search(index=index, body=body, size=size, from_=from_)

        hits = [hit["_source"] for hit in resp["hits"]["hits"]]
        total = resp["hits"]["total"]
        if isinstance(total, dict):
            total = total["value"]

        return SearchResult(
            hits=hits,
            total=total,
            truncated=total > size,
        )

    def scroll(self, index: str, query: Optional[dict] = None, size: int = 100, scroll: str = "5m") -> ScrollResult:
        body = query or {"query": {"match_all": {}}}
        resp = self.client.search(index=index, body=body, size=size, scroll=scroll)

        hits = [hit["_source"] for hit in resp["hits"]["hits"]]
        scroll_id = resp.get("_scroll_id")

        return ScrollResult(
            hits=hits,
            scroll_id=scroll_id,
            took_ms=resp.get("took", 0),
        )

    def scroll_next(self, scroll_id: str, scroll: str = "5m") -> ScrollResult:
        resp = self.client.scroll(scroll_id=scroll_id, scroll=scroll)

        hits = [hit["_source"] for hit in resp["hits"]["hits"]]
        new_scroll_id = resp.get("_scroll_id")

        return ScrollResult(
            hits=hits,
            scroll_id=new_scroll_id,
            took_ms=resp.get("took", 0),
        )

    def scroll_clear(self, scroll_id: str) -> None:
        self.client.clear_scroll(scroll_id=scroll_id)

    def index_doc(self, index: str, doc: dict, id_: Optional[str] = None) -> str:
        kwargs = {"body": doc, "index": index}
        if id_:
            kwargs["id"] = id_
            result = self.client.index(**kwargs)
        else:
            result = self.client.index(**kwargs)
        return result["_id"]

    def update_doc(self, index: str, id_: str, doc: dict) -> None:
        self.client.update(index=index, id=id_, body={"doc": doc})

    def delete_doc(self, index: str, id_: str) -> None:
        self.client.delete(index=index, id=id_)

    def bulk(self, operations: list[dict], refresh: bool = False) -> BulkResult:
        resp = self.client.bulk(body=operations, refresh=refresh)

        succeeded = 0
        failed = 0
        errors = []

        for item in resp.get("items", []):
            action = item.get("index") or item.get("update") or item.get("delete") or item.get("create")
            if action.get("error"):
                failed += 1
                errors.append({
                    "action": list(item.keys())[0],
                    "id": action.get("_id"),
                    "error": action["error"].get("reason", "Unknown error"),
                })
            else:
                succeeded += 1

        return BulkResult(succeeded=succeeded, failed=failed, errors=errors)

    def list_indices(self) -> list[dict]:
        resp = self.client.cat.indices(format="json")

        indices = []
        for idx in resp:
            name = idx.get("index") or idx.get("uuid")
            if not name:
                continue

            docs_count = idx.get("docs.count") or 0
            docs_count = int(docs_count) if docs_count else 0

            size = idx.get("store.size") or "0b"

            indices.append({
                "name": name,
                "docs_count": docs_count,
                "size": size,
                "health": idx.get("health", "unknown"),
            })
        return indices

    def get_index_mapping(self, index: str) -> dict:
        return self.client.indices.get_mapping(index=index)

    def create_index(self, index: str, mappings: Optional[dict] = None, settings: Optional[dict] = None) -> dict:
        body = {}
        if mappings:
            body["mappings"] = mappings
        if settings:
            body["settings"] = settings
        return self.client.indices.create(index=index, body=body if body else None)

    def delete_index(self, index: str) -> bool:
        try:
            self.client.indices.delete(index=index)
            return True
        except NotFoundError:
            return False

    def index_exists(self, index: str) -> bool:
        return self.client.indices.exists(index=index)


class ES8Ops:
    """ES 8+ 操作类（使用 elasticsearch-py 8.x）."""

    def __init__(self, client: Elasticsearch):
        self.client = client

    def ping(self) -> bool:
        return self.client.ping()

    def search(self, index: str, query: Optional[dict] = None, size: int = 10, from_: int = 0) -> SearchResult:
        body = query or {"query": {"match_all": {}}}
        # ES 8+ 移除 _type
        body = {k: v for k, v in body.items() if k != "_type"}

        resp = self.client.search(index=index, body=body, size=size, from_=from_)

        hits = [hit["_source"] for hit in resp["hits"]["hits"]]
        total = resp["hits"]["total"]
        if isinstance(total, dict):
            total = total["value"]

        return SearchResult(
            hits=hits,
            total=total,
            truncated=total > size,
        )

    def scroll(self, index: str, query: Optional[dict] = None, size: int = 100, scroll: str = "5m") -> ScrollResult:
        body = query or {"query": {"match_all": {}}}
        body = {k: v for k, v in body.items() if k != "_type"}

        resp = self.client.search(index=index, body=body, size=size, scroll=scroll)

        hits = [hit["_source"] for hit in resp["hits"]["hits"]]
        scroll_id = resp.get("_scroll_id")

        return ScrollResult(
            hits=hits,
            scroll_id=scroll_id,
            took_ms=resp.get("took", 0),
        )

    def scroll_next(self, scroll_id: str, scroll: str = "5m") -> ScrollResult:
        resp = self.client.scroll(scroll_id=scroll_id, scroll=scroll)

        hits = [hit["_source"] for hit in resp["hits"]["hits"]]
        new_scroll_id = resp.get("_scroll_id")

        return ScrollResult(
            hits=hits,
            scroll_id=new_scroll_id,
            took_ms=resp.get("took", 0),
        )

    def scroll_clear(self, scroll_id: str) -> None:
        self.client.clear_scroll(scroll_id=scroll_id)

    def index_doc(self, index: str, doc: dict, id_: Optional[str] = None) -> str:
        kwargs = {"index": index, "body": doc}
        if id_:
            kwargs["id"] = id_
            result = self.client.index(**kwargs)
        else:
            result = self.client.index(**kwargs)
        return result["_id"]

    def update_doc(self, index: str, id_: str, doc: dict) -> None:
        self.client.update(index=index, id=id_, body={"doc": doc})

    def delete_doc(self, index: str, id_: str) -> None:
        self.client.delete(index=index, id=id_)

    def bulk(self, operations: list[dict], refresh: bool = False) -> BulkResult:
        resp = self.client.bulk(body=operations, refresh=refresh)

        succeeded = 0
        failed = 0
        errors = []

        for item in resp.get("items", []):
            action = item.get("index") or item.get("update") or item.get("delete") or item.get("create")
            if action.get("error"):
                failed += 1
                errors.append({
                    "action": list(item.keys())[0],
                    "id": action.get("_id"),
                    "error": action["error"].get("reason", "Unknown error"),
                })
            else:
                succeeded += 1

        return BulkResult(succeeded=succeeded, failed=failed, errors=errors)

    def list_indices(self) -> list[dict]:
        resp = self.client.cat.indices(format="json")

        indices = []
        for idx in resp:
            name = idx.get("index") or idx.get("uuid")
            if not name:
                continue

            docs_count = idx.get("docs.count") or 0
            docs_count = int(docs_count) if docs_count else 0

            size = idx.get("store.size") or "0b"

            indices.append({
                "name": name,
                "docs_count": docs_count,
                "size": size,
                "health": idx.get("health", "unknown"),
            })
        return indices

    def get_index_mapping(self, index: str) -> dict:
        return self.client.indices.get_mapping(index=index)

    def create_index(self, index: str, mappings: Optional[dict] = None, settings: Optional[dict] = None) -> dict:
        body = {}
        if mappings:
            body["mappings"] = mappings
        if settings:
            body["settings"] = settings
        return self.client.indices.create(index=index, body=body if body else None)

    def delete_index(self, index: str) -> bool:
        try:
            self.client.indices.delete(index=index)
            return True
        except NotFoundError:
            return False

    def index_exists(self, index: str) -> bool:
        return self.client.indices.exists(index=index)


# ============== 版本路由工厂 ==============

def create_ops(config: ConnectionConfig, es_version: int):
    """根据 ES 版本创建对应的操作类.

    路由策略：
    - ES 6.x: 使用 ES6Ops（原生 HTTP）
    - ES 7.x: 使用 ES7Ops（elasticsearch-py 7.x）
    - ES 8.x+: 使用 ES8Ops（elasticsearch-py 8.x）
    """
    if es_version == 6:
        return ES6Ops(
            url=config.url,
            username=config.username,
            password=config.password,
        )
    elif es_version == 7:
        client = create_client(config, es_version=7)
        return ES7Ops(client)
    else:
        # ES 8+ 也使用 ES7Ops 的逻辑，因为 elasticsearch-py 8.x API 与 7.x 基本兼容
        client = create_client(config, es_version=8)
        return ES8Ops(client)
