# -*- coding: utf-8 -*-
"""
yjm_monitor_probe.py

Windows 다중 모니터 진단 공통 모듈.

목적:
- mss가 보는 원본 모니터 목록을 그대로 확인한다.
- Win32 EnumDisplayMonitors가 보는 목록을 같이 확인한다.
- DPI awareness 적용 전/후 차이를 줄인다.
- yjm_win2rtc 본 프로그램과 별도 진단 GUI가 같은 함수를 사용하게 한다.

중요 원칙:
- 실제 캡처 기준은 mss.monitors 원본 순서를 우선한다.
- Win32 목록은 진단/비교용이다. mss 목록과 임의 병합하지 않는다.
"""

from __future__ import annotations

import ctypes
import json
import os
import platform
import sys
import time
from dataclasses import dataclass, asdict
from pathlib import Path
from typing import Any

import mss

try:
    import win32api
    import win32gui
except Exception:  # pragma: no cover - Windows 외 환경 진단용
    win32api = None
    win32gui = None


@dataclass
class MonitorInfo:
    api: str
    index: int
    left: int
    top: int
    width: int
    height: int
    right: int
    bottom: int
    primary: bool = False
    device: str = ""
    name: str = ""
    extra: dict[str, Any] | None = None

    @property
    def rect(self) -> tuple[int, int, int, int]:
        return self.left, self.top, self.right, self.bottom

    def to_dict(self) -> dict[str, Any]:
        return asdict(self)

    def label(self) -> str:
        primary = " / PRIMARY" if self.primary else ""
        dev = f" / {self.device}" if self.device else ""
        name = f" / {self.name}" if self.name else ""
        return (
            f"{self.api}[{self.index}] {self.width}x{self.height} "
            f"@ left={self.left}, top={self.top}, right={self.right}, bottom={self.bottom}"
            f"{primary}{dev}{name}"
        )


def set_dpi_awareness() -> str:
    """프로세스를 DPI aware로 만든다. 이미 설정되어 있으면 실패해도 무시한다."""
    if os.name != "nt":
        return "not-windows"

    # Windows 8.1+: Per Monitor DPI Aware
    try:
        ctypes.windll.shcore.SetProcessDpiAwareness(2)
        return "SetProcessDpiAwareness(2) OK"
    except Exception as exc1:
        # Windows Vista+: System DPI aware
        try:
            ctypes.windll.user32.SetProcessDPIAware()
            return f"SetProcessDPIAware OK / shcore failed: {exc1}"
        except Exception as exc2:
            return f"DPI awareness failed: shcore={exc1}, user32={exc2}"


def get_dpi_info() -> dict[str, Any]:
    info: dict[str, Any] = {}
    if os.name != "nt":
        return {"platform": "not-windows"}

    try:
        info["system_dpi_x"] = ctypes.windll.user32.GetDpiForSystem()
    except Exception as exc:
        info["system_dpi_x_error"] = str(exc)

    try:
        dc = ctypes.windll.user32.GetDC(0)
        # LOGPIXELSX=88, LOGPIXELSY=90
        info["device_caps_logpixelsx"] = ctypes.windll.gdi32.GetDeviceCaps(dc, 88)
        info["device_caps_logpixelsy"] = ctypes.windll.gdi32.GetDeviceCaps(dc, 90)
        ctypes.windll.user32.ReleaseDC(0, dc)
    except Exception as exc:
        info["device_caps_error"] = str(exc)

    return info


def list_mss_monitors() -> list[MonitorInfo]:
    """mss.monitors 원본 순서 그대로 반환. index 0은 전체 가상 바탕화면."""
    result: list[MonitorInfo] = []
    with mss.mss() as sct:
        for idx, mon in enumerate(sct.monitors):
            left = int(mon["left"])
            top = int(mon["top"])
            width = int(mon["width"])
            height = int(mon["height"])
            result.append(
                MonitorInfo(
                    api="mss",
                    index=idx,
                    left=left,
                    top=top,
                    width=width,
                    height=height,
                    right=left + width,
                    bottom=top + height,
                    primary=(idx == 1 and left == 0 and top == 0),
                    device="virtual" if idx == 0 else "",
                    extra=dict(mon),
                )
            )
    return result


def list_mss_physical_displays() -> list[MonitorInfo]:
    """캡처 소스 콤보박스용. mss[1:]만 반환하되 원본 순서를 유지한다."""
    return list_mss_monitors()[1:]


def list_win32_monitors() -> list[MonitorInfo]:
    result: list[MonitorInfo] = []
    if win32api is None:
        return result

    try:
        monitors = win32api.EnumDisplayMonitors()
    except Exception:
        return result

    for idx, item in enumerate(monitors):
        try:
            hmon = item[0]
            fallback_rect = item[2]
            info = win32api.GetMonitorInfo(hmon)
            left, top, right, bottom = info.get("Monitor", fallback_rect)
            work = info.get("Work")
            flags = int(info.get("Flags", 0))
            device = str(info.get("Device", ""))
            result.append(
                MonitorInfo(
                    api="win32",
                    index=idx,
                    left=int(left),
                    top=int(top),
                    width=int(right - left),
                    height=int(bottom - top),
                    right=int(right),
                    bottom=int(bottom),
                    primary=bool(flags & 1),
                    device=device,
                    extra={"work": work, "flags": flags},
                )
            )
        except Exception:
            continue
    return result


def list_display_devices() -> list[dict[str, Any]]:
    """EnumDisplayDevices 정보. DISPLAY1/2 이름 확인용."""
    result: list[dict[str, Any]] = []
    if win32api is None:
        return result
    try:
        i = 0
        while True:
            dev = win32api.EnumDisplayDevices(None, i, 0)
            result.append(
                {
                    "index": i,
                    "DeviceName": getattr(dev, "DeviceName", ""),
                    "DeviceString": getattr(dev, "DeviceString", ""),
                    "StateFlags": getattr(dev, "StateFlags", 0),
                    "DeviceID": getattr(dev, "DeviceID", ""),
                    "DeviceKey": getattr(dev, "DeviceKey", ""),
                }
            )
            i += 1
    except Exception:
        pass
    return result


def get_virtual_bounds_from_monitors(monitors: list[MonitorInfo]) -> dict[str, int]:
    if not monitors:
        return {"left": 0, "top": 0, "right": 0, "bottom": 0, "width": 0, "height": 0}
    left = min(m.left for m in monitors)
    top = min(m.top for m in monitors)
    right = max(m.right for m in monitors)
    bottom = max(m.bottom for m in monitors)
    return {
        "left": left,
        "top": top,
        "right": right,
        "bottom": bottom,
        "width": right - left,
        "height": bottom - top,
    }


def make_probe_report() -> dict[str, Any]:
    dpi_status = set_dpi_awareness()
    mss_monitors = list_mss_monitors()
    win32_monitors = list_win32_monitors()
    devices = list_display_devices()
    return {
        "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
        "python": sys.version,
        "executable": sys.executable,
        "platform": platform.platform(),
        "dpi_status": dpi_status,
        "dpi_info": get_dpi_info(),
        "mss_monitors": [m.to_dict() for m in mss_monitors],
        "mss_virtual_bounds_from_physical": get_virtual_bounds_from_monitors(mss_monitors[1:]),
        "win32_monitors": [m.to_dict() for m in win32_monitors],
        "win32_virtual_bounds": get_virtual_bounds_from_monitors(win32_monitors),
        "display_devices": devices,
    }


def format_report_text(report: dict[str, Any]) -> str:
    lines: list[str] = []
    lines.append("# yjm_monitor_probe report")
    lines.append(f"timestamp: {report.get('timestamp')}")
    lines.append(f"python: {report.get('python')}")
    lines.append(f"executable: {report.get('executable')}")
    lines.append(f"platform: {report.get('platform')}")
    lines.append(f"dpi_status: {report.get('dpi_status')}")
    lines.append(f"dpi_info: {report.get('dpi_info')}")
    lines.append("")

    lines.append("## MSS monitors")
    lines.append("주의: mss[0]은 전체 가상 바탕화면, mss[1:]가 실제 디스플레이 후보입니다.")
    for m in report.get("mss_monitors", []):
        mon = MonitorInfo(
            api=m.get("api", "mss"),
            index=int(m.get("index", 0)),
            left=int(m.get("left", 0)),
            top=int(m.get("top", 0)),
            width=int(m.get("width", 0)),
            height=int(m.get("height", 0)),
            right=int(m.get("right", 0)),
            bottom=int(m.get("bottom", 0)),
            primary=bool(m.get("primary", False)),
            device=str(m.get("device", "")),
            name=str(m.get("name", "")),
            extra=m.get("extra"),
        )
        lines.append("  " + mon.label())
    lines.append(f"mss_virtual_bounds_from_physical: {report.get('mss_virtual_bounds_from_physical')}")
    lines.append("")

    lines.append("## Win32 monitors")
    for m in report.get("win32_monitors", []):
        mon = MonitorInfo(
            api=m.get("api", "win32"),
            index=int(m.get("index", 0)),
            left=int(m.get("left", 0)),
            top=int(m.get("top", 0)),
            width=int(m.get("width", 0)),
            height=int(m.get("height", 0)),
            right=int(m.get("right", 0)),
            bottom=int(m.get("bottom", 0)),
            primary=bool(m.get("primary", False)),
            device=str(m.get("device", "")),
            name=str(m.get("name", "")),
            extra=m.get("extra"),
        )
        lines.append("  " + mon.label())
    lines.append(f"win32_virtual_bounds: {report.get('win32_virtual_bounds')}")
    lines.append("")

    lines.append("## Display devices")
    for d in report.get("display_devices", []):
        lines.append(
            f"  [{d.get('index')}] {d.get('DeviceName')} / {d.get('DeviceString')} / flags={d.get('StateFlags')}"
        )
    lines.append("")

    return "\n".join(lines)


def save_probe_report(out_dir: str | Path = "_monitor_probe_output") -> tuple[Path, Path]:
    out = Path(out_dir)
    out.mkdir(parents=True, exist_ok=True)
    report = make_probe_report()
    json_path = out / "monitor_report.json"
    txt_path = out / "monitor_report.txt"
    json_path.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
    txt_path.write_text(format_report_text(report), encoding="utf-8")
    return json_path, txt_path


if __name__ == "__main__":
    report = make_probe_report()
    print(format_report_text(report))
