#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
模块名称：system.py
模块描述：系统管理 API 路由（系统信息、运行环境）

作者：czj
创建日期：2026-07-29
最后修改：2026-07-29
"""

import ipaddress
import logging
import platform
import sys
import time
from pathlib import Path
from typing import Optional
from urllib.parse import urlparse

from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy import func, select, text
from sqlalchemy.ext.asyncio import AsyncSession

from app.config import settings
from app.database import get_db
from app.models.module import Module
from app.models.test_case import TestCase
from app.models.dingtalk_config import DingTalkConfig
from app.schemas.dingtalk_config import (
    DingTalkConfigResponse,
    DingTalkConfigUpdateRequest,
    SutConfigResponse,
    SutConfigUpdateRequest,
)

logger = logging.getLogger(__name__)

router = APIRouter()

# 应用启动时间（模块加载时记录）
_START_TIME = time.time()


def _validate_sut_url(url: str) -> Optional[str]:
    """
    校验被测系统 URL（防止无效地址保存后导致全部 UI 用例执行瘫痪）。

    规则：
    - 必须 http/https 且带主机名
    - 端口（如有）必须是合法 1-65535 数字（urlparse.port 会拒绝如 :abc）
    - 主机必须是合法 IPv4/IPv6 或合法域名；纯数字点分形式必须是合法 IPv4
      （如 `192.168.5.4202` 每段须 0-255，否则拒绝）

    Args:
        url (str): 待校验 URL（应已 strip）

    Returns:
        Optional[str]: 错误信息；校验通过返回 None
    """
    if not url.startswith(("http://", "https://")):
        return "URL 须以 http:// 或 https:// 开头"

    parsed = urlparse(url)
    host = parsed.hostname
    if not host:
        return "URL 缺少主机名"

    try:
        port = parsed.port
    except ValueError:
        return "URL 端口无效（端口须为 1-65535 的数字）"
    if port is not None and not (1 <= port <= 65535):
        return "URL 端口必须在 1-65535 之间"

    # 主机为合法 IP（IPv4/IPv6）则通过
    try:
        ipaddress.ip_address(host)
        return None
    except ValueError:
        pass

    # 纯数字点分形式（如 192.168.5.4202）必须是合法 IPv4——ipaddress 已失败，直接拒绝
    if all(ch in "0123456789." for ch in host):
        return "URL 主机无效（IP 地址每段须为 0-255）"

    # 域名校验：字母/数字/中划线/点，标签 1-63 字符，不以中划线开头结尾
    if len(host) > 253:
        return "URL 主机名过长"
    labels = host.rstrip(".").split(".")
    if not labels:
        return "URL 主机名无效"
    for label in labels:
        if not (1 <= len(label) <= 63):
            return "URL 主机名标签长度须为 1-63"
        if not all(ch.isalnum() or ch in "-_" for ch in label):
            return "URL 主机名包含非法字符"
        if label.startswith("-") or label.endswith("-"):
            return "URL 主机名标签不能以中划线开头或结尾"
    return None


@router.get("/info", response_model=dict)
async def get_system_info(db: AsyncSession = Depends(get_db)):
    """
    获取系统运行信息

    返回应用版本、运行环境、数据库状态、用例/模块统计等。

    Returns:
        dict: 系统信息
    """
    # 数据库类型
    is_sqlite = "sqlite" in settings.DATABASE_URL
    db_type = "sqlite" if is_sqlite else "mysql"

    # 数据库大小
    db_size_mb = 0.0
    try:
        if is_sqlite:
            # SQLite：直接读取文件大小
            db_path = settings.DATABASE_URL.replace("sqlite+aiosqlite:///", "")
            # 处理相对路径
            p = Path(db_path)
            if not p.is_absolute():
                # 相对路径通常相对 backend/ 目录
                p = Path.cwd() / p
            if p.exists():
                db_size_mb = round(p.stat().st_size / (1024 * 1024), 2)
        else:
            # MySQL：查询 information_schema
            result = await db.execute(
                text(
                    "SELECT ROUND(SUM(data_length + index_length) / 1024 / 1024, 2) "
                    "AS size_mb FROM information_schema.tables "
                    "WHERE table_schema = DATABASE()"
                )
            )
            row = result.scalar()
            db_size_mb = float(row) if row else 0.0
    except Exception as e:
        logger.warning(f"获取数据库大小失败: {e}")
        db_size_mb = 0.0

    # 模块 / 用例总数
    total_modules = 0
    total_cases = 0
    try:
        result = await db.execute(select(func.count(Module.id)))
        total_modules = result.scalar() or 0
        result = await db.execute(select(func.count(TestCase.id)))
        total_cases = result.scalar() or 0
    except Exception as e:
        logger.warning(f"获取模块/用例统计失败: {e}")

    # 运行时间
    uptime_seconds = int(time.time() - _START_TIME)
    hours, remainder = divmod(uptime_seconds, 3600)
    minutes, seconds = divmod(remainder, 60)
    uptime_text = f"{hours}小时{minutes}分钟{seconds}秒" if hours else f"{minutes}分钟{seconds}秒"

    return {
        "app_name": settings.APP_NAME,
        "backend_version": settings.APP_VERSION,
        "python_version": platform.python_version(),
        "platform": platform.platform(),
        "os": f"{platform.system()} {platform.release()}",
        "uptime_seconds": uptime_seconds,
        "uptime_text": uptime_text,
        "db_type": db_type,
        "db_size_mb": db_size_mb,
        "total_modules": total_modules,
        "total_cases": total_cases,
        "debug": settings.DEBUG,
        "playwright_headless": settings.PLAYWRIGHT_HEADLESS,
        "frontend_version": "1.0.0",
    }


# ==================== 钉钉对接配置 ====================


@router.get("/dingtalk-config", response_model=DingTalkConfigResponse, summary="获取钉钉对接配置")
async def get_dingtalk_config(db: AsyncSession = Depends(get_db)):
    """
    获取当前钉钉对接配置

    优先从数据库读取，若数据库无记录则回退到环境变量默认值。
    """
    result = await db.execute(select(DingTalkConfig).where(DingTalkConfig.id == 1))
    config = result.scalar_one_or_none()

    if config:
        # 数据库有记录，但空字段回退到环境变量默认值（保证配置自动填充）
        return DingTalkConfigResponse(
            app_key=config.app_key or settings.DINGTALK_APP_KEY or "",
            app_secret=config.app_secret or settings.DINGTALK_APP_SECRET or "",
            agent_id=config.agent_id or settings.DINGTALK_AGENT_ID or 0,
            target_url=config.target_url or settings.TARGET_URL or "",
        )

    # 数据库无记录，回退到环境变量
    return DingTalkConfigResponse(
        app_key=settings.DINGTALK_APP_KEY or "",
        app_secret=settings.DINGTALK_APP_SECRET or "",
        agent_id=settings.DINGTALK_AGENT_ID or 0,
        target_url=settings.TARGET_URL or "",
    )


@router.put("/dingtalk-config", response_model=DingTalkConfigResponse, summary="更新钉钉对接配置")
async def update_dingtalk_config(request: DingTalkConfigUpdateRequest, db: AsyncSession = Depends(get_db)):
    """
    更新钉钉对接配置（持久化到数据库）

    更新后的配置会持久化到数据库，服务重启后依然有效。
    """
    # 从数据库读取或创建新记录
    result = await db.execute(select(DingTalkConfig).where(DingTalkConfig.id == 1))
    config = result.scalar_one_or_none()

    if not config:
        # 新记录：先用环境变量默认值填充，再用请求值覆盖
        config = DingTalkConfig(
            id=1,
            app_key=settings.DINGTALK_APP_KEY or "",
            app_secret=settings.DINGTALK_APP_SECRET or "",
            agent_id=settings.DINGTALK_AGENT_ID or 0,
        )
        db.add(config)

    # 更新字段
    if request.app_key is not None:
        config.app_key = request.app_key
        settings.DINGTALK_APP_KEY = request.app_key
    if request.app_secret is not None:
        config.app_secret = request.app_secret
        settings.DINGTALK_APP_SECRET = request.app_secret
    if request.agent_id is not None:
        config.agent_id = request.agent_id
        settings.DINGTALK_AGENT_ID = request.agent_id
    if request.target_url is not None:
        config.target_url = request.target_url
        settings.TARGET_URL = request.target_url

    await db.commit()
    await db.refresh(config)

    logger.info("钉钉对接配置已更新（持久化到数据库）")

    return DingTalkConfigResponse(
        app_key=config.app_key or settings.DINGTALK_APP_KEY or "",
        app_secret=config.app_secret or settings.DINGTALK_APP_SECRET or "",
        agent_id=config.agent_id or settings.DINGTALK_AGENT_ID or 0,
        target_url=config.target_url or settings.TARGET_URL or "",
    )


# ==================== 被测系统配置 ====================


@router.get("/sut-config", response_model=SutConfigResponse, summary="获取被测系统配置")
async def get_sut_config(db: AsyncSession = Depends(get_db)):
    """
    获取被测系统访问 URL 与认证接口路径

    优先读取数据库持久化的值，无记录时回退到环境变量默认值。
    """
    result = await db.execute(select(DingTalkConfig).where(DingTalkConfig.id == 1))
    config = result.scalar_one_or_none()

    login_path = settings.AUTH_LOGIN_PATH or "/platform/api/auth/login"
    captcha_path = settings.AUTH_CAPTCHA_PATH or "/platform/api/code"

    if config and config.target_url:
        return SutConfigResponse(
            url=config.target_url,
            source="db",
            login_path=config.login_path or login_path,
            captcha_path=config.captcha_path or captcha_path,
        )

    return SutConfigResponse(
        url=settings.TARGET_URL or "",
        source="env",
        login_path=login_path,
        captcha_path=captcha_path,
    )


@router.put("/sut-config", response_model=SutConfigResponse, summary="更新被测系统配置")
async def update_sut_config(
    request: SutConfigUpdateRequest,
    db: AsyncSession = Depends(get_db),
):
    """
    更新被测系统访问 URL 与认证接口路径

    持久化到数据库（dingtalk_configs）并立即更新运行时 settings 对应字段，
    性能测试/UI 自动化执行即使用该地址与认证路径；服务重启后仍生效
    （lifespan 启动时从数据库加载）。
    """
    url = (request.url or "").strip().rstrip("/")
    url_err = _validate_sut_url(url)
    if url_err:
        raise HTTPException(status_code=400, detail=url_err)

    # 从数据库读取或创建单行记录（id=1）
    result = await db.execute(select(DingTalkConfig).where(DingTalkConfig.id == 1))
    config = result.scalar_one_or_none()
    if not config:
        config = DingTalkConfig(
            id=1,
            app_key=settings.DINGTALK_APP_KEY or "",
            app_secret=settings.DINGTALK_APP_SECRET or "",
            agent_id=settings.DINGTALK_AGENT_ID or 0,
        )
        db.add(config)

    config.target_url = url
    settings.TARGET_URL = url

    # 认证接口路径（未传时保持现有值）
    if request.login_path is not None:
        login_path = request.login_path.strip()
        if not login_path.startswith("/"):
            raise HTTPException(status_code=400, detail="登录接口路径须以 / 开头")
        config.login_path = login_path
        settings.AUTH_LOGIN_PATH = login_path

    if request.captcha_path is not None:
        captcha_path = request.captcha_path.strip()
        if not captcha_path.startswith("/"):
            raise HTTPException(status_code=400, detail="验证码接口路径须以 / 开头")
        config.captcha_path = captcha_path
        settings.AUTH_CAPTCHA_PATH = captcha_path

    await db.commit()
    await db.refresh(config)
    logger.info(f"被测系统配置已更新并持久化: url={url}, login_path={config.login_path}, captcha_path={config.captcha_path}")

    return SutConfigResponse(
        url=config.target_url,
        source="db",
        login_path=config.login_path or "/platform/api/auth/login",
        captcha_path=config.captcha_path or "/platform/api/code",
    )
