"""
MT5 ストラテジーテスター / トレードレポート HTML から window.__report の JSON を取り出す。
（本サイトの downloads 配布用。リポジトリの scripts/mt5_report_extract.py と同等の内容に同期すること）

正規表現の {.*?}; は JSON 内の '};' 部分文字列で誤マッチするため、JSONDecoder.raw_decode を使う。

window.__report が無い旧式 HTML（テーブルのみのストラテジーテスターレポート）は、
「約定」表の 時間・残高 列から Balance 曲線を再構成する。
"""
from __future__ import annotations

import json
import re
from datetime import datetime
from typing import Any


def read_mt5_report_html(html_path: str) -> str:
    """
    MT5 が UTF-8 / UTF-16 / CP932 等で保存した HTML を文字列として読む。
    """
    with open(html_path, "rb") as f:
        raw = f.read()
    if not raw:
        return ""
    if raw.startswith(b"\xff\xfe") or raw.startswith(b"\xfe\xff"):
        return raw.decode("utf-16")
    if raw.startswith(b"\xef\xbb\xbf"):
        return raw.decode("utf-8-sig")
    try:
        return raw.decode("utf-8")
    except UnicodeDecodeError:
        try:
            return raw.decode("utf-16-le")
        except UnicodeDecodeError:
            return raw.decode("cp932", errors="replace")


def _strip_td_inner_html(fragment: str) -> str:
    t = re.sub(r"<[^>]+>", "", fragment)
    t = t.replace("&nbsp;", " ").replace("\xa0", " ")
    return t.strip()


def _parse_mt5_number_with_spaces(s: str) -> float:
    s = s.replace(" ", "").replace(",", "").replace("\u202f", "")
    if not s or s == "—" or s == "-":
        return float("nan")
    return float(s)


_DATE_TIME_RE = re.compile(
    r"^\s*(\d{4})\.(\d{2})\.(\d{2})\s+(\d{2}):(\d{2}):(\d{2})\s*$"
)


def _legacy_strategy_tester_report(content: str) -> dict | None:
    """
    meta generator strategy tester のテーブル専用 HTML から
    generate_chart_from_report が期待する dict 形へ変換する。
    「約定 / Deals」表の各行の 残高 列を Balance 曲線にする（Equity は Balance と同一）。
    """
    lower = content.lower()
    if "strategy tester" not in lower and "ストラテジーテスター" not in content:
        return None

    marker = content.find("<b>約定</b>")
    if marker < 0:
        marker = content.find("<b>Deals</b>")
    if marker < 0:
        return None
    chunk = content[marker:]

    chart: list[dict[str, Any]] = []
    profit_factor = 0.0
    drawdown_pct = 0.0

    pf_m = re.search(
        r"プロフィットファクター:</td>\s*<td[^>]*><b>([^<]+)</b>",
        content,
        re.IGNORECASE,
    )
    if not pf_m:
        pf_m = re.search(
            r"Profit factor:</td>\s*<td[^>]*><b>([^<]+)</b>",
            content,
            re.IGNORECASE,
        )
    if pf_m:
        try:
            profit_factor = float(_strip_td_inner_html(pf_m.group(1)).replace(",", ""))
        except ValueError:
            pass

    dd_m = re.search(
        r"残高相対ドローダウン:</td>\s*<td[^>]*><b>([^<]+)</b>",
        content,
        re.IGNORECASE,
    )
    if not dd_m:
        dd_m = re.search(
            r"Balance relative drawdown:</td>\s*<td[^>]*><b>([^<]+)</b>",
            content,
            re.IGNORECASE,
        )
    if dd_m:
        raw_dd = _strip_td_inner_html(dd_m.group(1))
        pct_m = re.match(r"([\d.]+)\s*%", raw_dd)
        if pct_m:
            try:
                drawdown_pct = float(pct_m.group(1)) / 100.0
            except ValueError:
                pass

    for tr in re.finditer(r"<tr[^>]*align=right[^>]*>(.*?)</tr>", chunk, re.IGNORECASE | re.DOTALL):
        tds = re.findall(r"<td[^>]*>(.*?)</td>", tr.group(1), re.DOTALL)
        if len(tds) < 12:
            continue
        t0 = _strip_td_inner_html(tds[0])
        if not _DATE_TIME_RE.match(t0):
            continue
        bal_str = _strip_td_inner_html(tds[11])
        try:
            balance = _parse_mt5_number_with_spaces(bal_str)
        except ValueError:
            continue
        if balance != balance:  # NaN
            continue
        try:
            dt = datetime.strptime(t0, "%Y.%m.%d %H:%M:%S")
        except ValueError:
            continue
        ts = int(dt.timestamp())
        chart.append({"x": ts, "y": [balance, balance]})

    if not chart:
        return None

    chart.sort(key=lambda p: p["x"])
    last_bal = chart[-1]["y"][0]

    return {
        "account": {"currency": "JPY"},
        "balance": {
            "balance": last_bal,
            "equity": last_bal,
            "chart": chart,
        },
        "summaryIndicators": {
            "drawdown": drawdown_pct,
            "profit_factor": profit_factor,
        },
    }


def extract_report_dict_from_html(html_path: str) -> dict:
    """
    window.__report に代入されている JSON オブジェクトを dict として返す。
    テーブルだけの旧ストラテジーテスター HTML は「約定」表から再構成する。
    """
    content = read_mt5_report_html(html_path)
    m = re.search(r"window\.__report\s*=\s*", content)
    if not m:
        legacy = _legacy_strategy_tester_report(content)
        if legacy is not None:
            return legacy
        raise ValueError(
            f"Could not find window.__report in {html_path}, "
            "and could not parse legacy Strategy Tester table HTML. "
            "Save the report from Strategy Tester again, or use an HTML that contains "
            "embedded JSON (newer MT5) or the standard table export with a Deals section."
        )
    pos = m.end()
    while pos < len(content) and content[pos] in " \t\r\n":
        pos += 1
    if pos >= len(content):
        raise ValueError(f"Could not find JSON object after window.__report in {html_path}")
    if content[pos] != "{":
        brace = content.find("{", m.start())
        if brace < 0:
            raise ValueError(f"Could not find opening '{{' for report JSON in {html_path}")
        pos = brace
    try:
        data, _ = json.JSONDecoder().raw_decode(content, pos)
    except json.JSONDecodeError as e:
        raise ValueError(f"Could not parse report JSON in {html_path}: {e}") from e
    if not isinstance(data, dict):
        raise ValueError("Report root is not a JSON object")
    return data
