from __future__ import annotations

import json
import os
import sys
import tempfile
import urllib.error
import urllib.request
from pathlib import Path
from typing import Any


DEFAULT_AI_BASE_URL = "https://api.deepseek.com"
DEFAULT_AI_MODEL = "deepseek-v4-flash"
MDM_CLOUD_SERVICE_DIRS = {
    "admin": "admin",
    "auth": "auth",
    "base-service": "base",
    "bi-dv-service": "bi-dv",
    "file-service": "file",
    "gateway": "gateway",
    "gtx-service": "gtx",
    "mdm-esb-service": "mdm-esb",
    "mdm-model-service-yapp": "mdm-model",
    "mdm-quality-service": "mdm-quality",
    "xxl-job-service": "xxl-job",
}


class AIError(Exception):
    """Raised when AI configuration or API calls fail."""


def get_app_dir() -> Path:
    if getattr(sys, "frozen", False):
        return Path(sys.executable).resolve().parent
    return Path(__file__).resolve().parent


class AIConfigStore:
    def __init__(self, data_dir: Path | None = None) -> None:
        self.data_dir = data_dir or get_app_dir()
        self.path = self.data_dir / "ai_config.json"

    def load(self) -> dict[str, str]:
        if not self.path.exists():
            return {
                "base_url": DEFAULT_AI_BASE_URL,
                "model": DEFAULT_AI_MODEL,
                "api_key": "",
            }
        try:
            raw = json.loads(self.path.read_text(encoding="utf-8"))
        except (OSError, json.JSONDecodeError) as exc:
            raise AIError(f"读取 ai_config.json 失败：{exc}") from exc
        if not isinstance(raw, dict):
            raise AIError("ai_config.json 格式错误，应为对象。")
        return {
            "base_url": str(raw.get("base_url", DEFAULT_AI_BASE_URL)).strip()
            or DEFAULT_AI_BASE_URL,
            "model": str(raw.get("model", DEFAULT_AI_MODEL)).strip()
            or DEFAULT_AI_MODEL,
            "api_key": str(raw.get("api_key", "")).strip(),
        }

    def save(self, config: dict[str, str]) -> None:
        base_url = config.get("base_url", "").strip() or DEFAULT_AI_BASE_URL
        model = config.get("model", "").strip() or DEFAULT_AI_MODEL
        api_key = config.get("api_key", "").strip()
        data = {
            "base_url": base_url.rstrip("/"),
            "model": model,
            "api_key": api_key,
        }
        self.data_dir.mkdir(parents=True, exist_ok=True)
        fd, tmp_name = tempfile.mkstemp(
            prefix=".ai_config.json.", suffix=".tmp", dir=str(self.data_dir)
        )
        tmp_path = Path(tmp_name)
        try:
            with os.fdopen(fd, "w", encoding="utf-8") as file:
                json.dump(data, file, ensure_ascii=False, indent=2)
                file.write("\n")
            os.replace(tmp_path, self.path)
        except OSError as exc:
            raise AIError(f"写入 ai_config.json 失败：{exc}") from exc
        finally:
            if tmp_path.exists():
                try:
                    tmp_path.unlink()
                except OSError:
                    pass


class DeepSeekClient:
    def __init__(self, base_url: str, model: str, api_key: str) -> None:
        self.base_url = base_url.rstrip("/")
        self.model = model
        self.api_key = api_key

    def analyze_services(self, project: dict[str, str], raw_listing: str) -> dict[str, Any]:
        if not self.api_key:
            raise AIError("请先配置 DeepSeek API Key。")
        if not raw_listing.strip():
            raise AIError("请先粘贴服务器 jar 列表或目录列表。")

        payload = {
            "model": self.model,
            "messages": [
                {
                    "role": "system",
                    "content": SERVICE_ANALYSIS_SYSTEM_PROMPT,
                },
                {
                    "role": "user",
                    "content": build_service_analysis_prompt(project, raw_listing),
                },
            ],
            "response_format": {"type": "json_object"},
            "stream": False,
            "temperature": 0.1,
            "max_tokens": 4096,
        }
        response = self._post_json("/chat/completions", payload)
        try:
            content = response["choices"][0]["message"]["content"]
        except (KeyError, IndexError, TypeError) as exc:
            raise AIError("AI 返回格式异常，未找到 message.content。") from exc

        try:
            result = json.loads(content)
        except json.JSONDecodeError as exc:
            raise AIError(f"AI 未返回有效 JSON：{exc}") from exc
        if not isinstance(result, dict):
            raise AIError("AI 返回 JSON 格式错误，应为对象。")
        return normalize_ai_result(result)

    def parse_log_time(self, user_text: str, current_date: str) -> dict[str, str]:
        if not self.api_key:
            raise AIError("请先配置 DeepSeek API Key。")
        if not user_text.strip():
            raise AIError("请先输入日志截取时间描述。")

        payload = {
            "model": self.model,
            "messages": [
                {
                    "role": "system",
                    "content": LOG_TIME_SYSTEM_PROMPT,
                },
                {
                    "role": "user",
                    "content": (
                        f"当前日期：{current_date}\n"
                        f"用户描述：{user_text}\n"
                        "请解析日志日期、开始时间、结束时间。"
                    ),
                },
            ],
            "response_format": {"type": "json_object"},
            "stream": False,
            "temperature": 0,
            "max_tokens": 1024,
        }
        response = self._post_json("/chat/completions", payload)
        try:
            content = response["choices"][0]["message"]["content"]
            result = json.loads(content)
        except (KeyError, IndexError, TypeError, json.JSONDecodeError) as exc:
            raise AIError("AI 未返回有效日志时间 JSON。") from exc
        return normalize_log_time_result(result)

    def _post_json(self, path: str, payload: dict[str, Any]) -> dict[str, Any]:
        url = f"{self.base_url}{path}"
        body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
        request = urllib.request.Request(
            url,
            data=body,
            headers={
                "Content-Type": "application/json",
                "Authorization": f"Bearer {self.api_key}",
            },
            method="POST",
        )
        try:
            with urllib.request.urlopen(request, timeout=60) as response:
                return json.loads(response.read().decode("utf-8"))
        except urllib.error.HTTPError as exc:
            detail = exc.read().decode("utf-8", errors="replace")
            raise AIError(f"AI 接口请求失败：HTTP {exc.code} {detail}") from exc
        except urllib.error.URLError as exc:
            raise AIError(f"AI 接口连接失败：{exc.reason}") from exc
        except TimeoutError as exc:
            raise AIError("AI 接口请求超时。") from exc
        except json.JSONDecodeError as exc:
            raise AIError(f"AI 接口返回非 JSON 内容：{exc}") from exc


SERVICE_ANALYSIS_SYSTEM_PROMPT = """
你是一个 MDM 运维服务识别助手。你只输出 json，不输出 markdown。
你的任务是从用户提供的服务器目录列表、find 结果、ls 结果或 jar 文件列表中识别 MDM 项目的 Java 服务。

必须输出这个 JSON 对象格式：
{
  "detected_mdm_version": "MDM9 或 MDM12 或 MDM13 或 unknown",
  "detected_jar_version": "例如 13.2.0.0，未知则为空字符串",
  "summary": "一句话概括识别结果",
  "services": [
    {
      "service_name": "服务名",
      "jar_name": "jar 文件名",
      "jar_version": "jar 版本号，未知则为空字符串",
      "deploy_dir": "部署目录，以 / 结尾",
      "is_custom": false,
      "risk": "风险说明，没有则为空字符串"
    }
  ],
  "missing_standard_services": ["缺失的标准服务名"],
  "extra_custom_services": ["额外或定制服务名"],
  "version_conflicts": ["版本不一致说明"],
  "risks": ["整体风险提示"]
}

识别规则：
- MDM9 通常是 i-bpm-6.1.jar、i-console-6.1.jar、mdm-engine-6.1.jar 这类包，常见目录 /u02/jars/。
- MDM12/MDM13 通常是 sunway-cloud-服务名-版本.jar，例如 sunway-cloud-admin-13.2.0.0.jar。
- MDM12/MDM13 标准部署目录在 /home/mdm/ 下，目录名不是完整服务名，而是这些短目录：admin/admin、auth/auth、base-service/base、bi-dv-service/bi-dv、file-service/file、gateway/gateway、gtx-service/gtx、mdm-esb-service/mdm-esb、mdm-model-service-yapp/mdm-model、mdm-quality-service/mdm-quality、xxl-job-service/xxl-job。
- /home/mdm/startAll.sh 是启动脚本，不是 Java 服务，不要放入 services。
- 如果输入来自 ps -ef | grep sunway，优先识别 java 命令中的 -jar 后面的绝对 jar 路径，例如 /data/mdm/mdm-esb/sunway-cloud-mdm-esb-service-13.2.0.0.jar，此时 deploy_dir 必须使用真实目录 /data/mdm/mdm-esb/，不要改成默认目录。
- 如果服务名不符合标准服务，或包含项目定制后缀，例如 yapp，需要标记 is_custom=true。
- deploy_dir 应从路径中提取；如果输入只有 jar 文件名，根据版本推断默认目录。
- 不要编造不存在的 jar 文件；只能基于输入内容和明确规则推断。
"""


LOG_TIME_SYSTEM_PROMPT = """
你是日志截取时间解析助手。你只输出 json，不输出 markdown。
你需要把用户的自然语言时间描述解析为日志日期和起止时间。

必须输出：
{
  "log_date": "YYYY-MM-DD",
  "start_time": "HH:MM:SS",
  "end_time": "HH:MM:SS",
  "explanation": "简短说明"
}

规则：
- 如果用户说今天，使用当前日期。
- 如果用户说昨天，使用当前日期减一天。
- 如果只说 10点到10点半，输出 10:00:00 到 10:30:00。
- 如果只说 10:05 到 10:20，补秒为 00。
- 如果缺少日期，默认使用当前日期。
- 如果缺少结束时间，默认结束时间为开始时间后 10 分钟。
- 不要生成命令，只解析时间。
"""


def build_service_analysis_prompt(project: dict[str, str], raw_listing: str) -> str:
    return f"""
请用 json 分析以下项目服务列表。

项目名称：{project.get("project_name", "")}
项目 MDM 版本：{project.get("mdm_version", "")}

原始输入：
{raw_listing}
"""


def normalize_ai_result(result: dict[str, Any]) -> dict[str, Any]:
    services = result.get("services", [])
    normalized_services: list[dict[str, Any]] = []
    if isinstance(services, list):
        for item in services:
            if not isinstance(item, dict):
                continue
            deploy_dir = str(item.get("deploy_dir", "")).strip()
            if not deploy_dir:
                deploy_dir = standard_deploy_dir(
                    str(item.get("service_name", "")), str(item.get("jar_name", ""))
                )
            if deploy_dir and not deploy_dir.endswith("/"):
                deploy_dir = f"{deploy_dir}/"
            normalized_services.append(
                {
                    "service_name": str(item.get("service_name", "")).strip(),
                    "jar_name": str(item.get("jar_name", "")).strip(),
                    "jar_version": str(item.get("jar_version", "")).strip(),
                    "deploy_dir": deploy_dir,
                    "is_custom": bool(item.get("is_custom", False)),
                    "risk": str(item.get("risk", "")).strip(),
                }
            )

    return {
        "detected_mdm_version": str(result.get("detected_mdm_version", "")).strip(),
        "detected_jar_version": str(result.get("detected_jar_version", "")).strip(),
        "summary": str(result.get("summary", "")).strip(),
        "services": normalized_services,
        "missing_standard_services": string_list(result.get("missing_standard_services", [])),
        "extra_custom_services": string_list(result.get("extra_custom_services", [])),
        "version_conflicts": string_list(result.get("version_conflicts", [])),
        "risks": string_list(result.get("risks", [])),
    }


def string_list(value: Any) -> list[str]:
    if not isinstance(value, list):
        return []
    return [str(item).strip() for item in value if str(item).strip()]


def normalize_log_time_result(result: dict[str, Any]) -> dict[str, str]:
    return {
        "log_date": str(result.get("log_date", "")).strip(),
        "start_time": str(result.get("start_time", "")).strip(),
        "end_time": str(result.get("end_time", "")).strip(),
        "explanation": str(result.get("explanation", "")).strip(),
    }


def standard_deploy_dir(service_name: str, jar_name: str) -> str:
    service = service_name.strip()
    if service in MDM_CLOUD_SERVICE_DIRS:
        return f"/home/mdm/{MDM_CLOUD_SERVICE_DIRS[service]}/"

    for candidate, directory in MDM_CLOUD_SERVICE_DIRS.items():
        if jar_name.startswith(f"sunway-cloud-{candidate}-"):
            return f"/home/mdm/{directory}/"
    return ""
