import asyncio
import hashlib
import json
import logging
import os
import socket
from collections.abc import AsyncIterator

import aiofiles
import aiohttp

logger = logging.getLogger(__name__)


class HikCameraControl:
    def __init__(self, ip, user, password, port=80):
        self.ip = ip
        self.port = port
        self.user = user
        self.password = password
        self.base_url = f"http://{ip}:{port}"

    def _parse_www_authenticate(self, header):
        """解析 401 响应头中的 Digest 参数"""
        params = {}
        content = header.replace("Digest ", "")
        parts = content.split(",")
        for part in parts:
            if "=" in part:
                key, val = part.strip().split("=", 1)
                params[key] = val.strip('"')
        return params

    def _get_digest_auth_header(self, method, uri, auth_info):
        """手动计算 Digest 认证头"""
        realm = auth_info.get("realm", "")
        nonce = auth_info.get("nonce", "")
        qop = auth_info.get("qop", "auth")
        cnonce = hashlib.md5(os.urandom(8)).hexdigest()[:16]
        nc = "00000001"

        ha1 = hashlib.md5(f"{self.user}:{realm}:{self.password}".encode()).hexdigest()
        ha2 = hashlib.md5(f"{method}:{uri}".encode()).hexdigest()
        response = hashlib.md5(
            f"{ha1}:{nonce}:{nc}:{cnonce}:{qop}:{ha2}".encode()
        ).hexdigest()

        return (
            f'Digest username="{self.user}", realm="{realm}", nonce="{nonce}", '
            f'uri="{uri}", response="{response}", qop={qop}, nc={nc}, cnonce="{cnonce}"'
        )

    async def request(self, method, uri, data=None, headers=None):
        """通用的异步请求处理逻辑（包含两次握手）"""
        url = f"{self.base_url}{uri}"
        if headers is None:
            headers = {}

        async with aiohttp.ClientSession() as session:
            # 1. 第一次请求：获取 401 挑战
            async with session.request(method, url, data=data) as resp:
                if resp.status != 401:
                    return resp.status, await resp.text()
                auth_info = self._parse_www_authenticate(
                    resp.headers.get("WWW-Authenticate", "")
                )

            # 2. 第二次请求：带上认证头
            headers["Authorization"] = self._get_digest_auth_header(
                method, uri, auth_info
            )
            async with session.request(method, url, data=data, headers=headers) as resp:
                assert resp.status != 401, "用户认证失败"
                return resp.status, await resp.text()

    async def ptz_move(self, pan, tilt=0, duration=1):
        """
        连续转动控制
        pan/tilt: -100 到 100
        duration: 转动持续时间（秒）
        """
        uri = "/ISAPI/PTZCtrl/channels/1/continuous"
        xml_template = (
            '<?xml version="1.0" encoding="UTF-8"?>'
            '<PTZData version="2.0" xmlns="http://www.isapi.org/ver20/XMLSchema">'
            "<pan>{p}</pan><tilt>{t}</tilt>"
            "</PTZData>"
        )

        logger.info(f"开始移动: pan={pan}...")
        await self.request("PUT", uri, data=xml_template.format(p=pan, t=tilt))
        await asyncio.sleep(duration)

        logger.info("停止移动...")
        await self.request("PUT", uri, data=xml_template.format(p=0, t=0))

    async def voice_close(self):
        await self.request("PUT", "/ISAPI/System/TwoWayAudio/channels/1/close")
        logger.info("会话关闭")

    async def voice_open(self):
        await self.request("PUT", "/ISAPI/System/TwoWayAudio/channels/1/open")
        logger.info("会话打开")

    async def get_token_for_audio_data(self):
        url = "/ISAPI/Security/token?format=json"
        status, token_resp = await self.request("GET", url)
        if status == 200:
            token = json.loads(token_resp)["Token"]["value"]
            return token
        else:
            logger.info("获取token失败")

    async def socket_send_task(self, date_generator: AsyncIterator, token):
        """高精度定时发送任务"""
        reader, writer = await asyncio.open_connection(self.ip, self.port)
        # 获取底层 socket 并开启保活
        sock = writer.get_extra_info("socket")
        sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)

        header = (
            f"PUT /ISAPI/System/TwoWayAudio/channels/1/audioData"
            f"?token={token} HTTP/1.1\r\n"
            f"Host: {self.ip}\r\n"
            f"User-Agent: NS-HTTP/1.0\r\n"
            f"Content-Type: application/octet-stream\r\n"
            f"Connection: keep-alive\r\n"
            f"Content-Length: 0\r\n\r\n"
        ).encode()

        writer.write(header)
        await writer.drain()

        interval = 0.02
        loop = asyncio.get_running_loop()
        next_time = loop.time()

        logger.info(">>> 开始音频推送...")
        try:
            # while True:
            async for chunk in date_generator:
                # chunk = await queue.get()
                # 核心改进：发送超时监控
                try:
                    writer.write(chunk)
                    # 如果 2 秒内无法将数据刷入 TCP 缓冲区，说明网络已断
                    await asyncio.wait_for(writer.drain(), timeout=2.0)
                except asyncio.TimeoutError:
                    logger.info("\n[错误] 检测到发送超时，网络可能已断开！")
                    raise ConnectionError("Network write timeout")

                next_time += interval
                delay = next_time - loop.time()
                if delay > 0:
                    await asyncio.sleep(delay)
                else:
                    next_time = loop.time()
        finally:
            writer.close()
            await writer.wait_closed()
            try:
                await asyncio.wait_for(writer.wait_closed(), timeout=1.0)
            except Exception:
                pass

    async def voice_file_generator(self, file_path):
        async with aiofiles.open(file_path, "rb") as f:
            while True:
                chunk = await f.read(160)
                if not chunk:
                    break
                yield chunk

    async def play_audio(self, file_path):
        await self.voice_open()
        token = await self.get_token_for_audio_data()
        try:
            await self.socket_send_task(self.voice_file_generator(file_path), token)
        except Exception:
            logger.exception("对讲中断")
        finally:
            try:
                await self.voice_close()
            except Exception:
                pass
            logger.info(">>> 对讲已结束并释放信道")
