#!/usr/bin/env python3
from __future__ import annotations

import argparse
import html
import json
import os
from datetime import UTC, datetime
from decimal import Decimal
from pathlib import Path
from typing import Any
from urllib.parse import quote

import clickhouse_connect
import uvicorn
from fastapi import FastAPI, HTTPException, Query
from fastapi.responses import HTMLResponse, PlainTextResponse, Response


def parse_args() -> argparse.Namespace:
    p = argparse.ArgumentParser(description="Read-only company intelligence API for analytics_1c")
    p.add_argument("--host", default=os.getenv("AW_1C_COMPANY_API_HOST", "127.0.0.1"))
    p.add_argument("--port", type=int, default=int(os.getenv("AW_1C_COMPANY_API_PORT", "8710")))
    return p.parse_args()


def q(value: str) -> str:
    return "'" + value.replace("'", "''") + "'"


def to_plain(value: Any) -> Any:
    if isinstance(value, Decimal):
        return float(value)
    if isinstance(value, datetime):
        return value.isoformat()
    return value


def rows_to_dict(result) -> list[dict[str, Any]]:
    return [
        {name: to_plain(value) for name, value in zip(result.column_names, row)}
        for row in result.result_rows
    ]


def ch_client():
    return clickhouse_connect.get_client(
        host=os.getenv("CLICKHOUSE_HOST", "localhost"),
        port=int(os.getenv("CLICKHOUSE_PORT", "8123")),
        username=os.getenv("CLICKHOUSE_USER", "default"),
        password=os.getenv("CLICKHOUSE_PASSWORD", ""),
        database=os.getenv("CLICKHOUSE_DB", "analytics_1c"),
    )


def manager_brief_state_dir() -> Path:
    root = Path(os.getenv("AW_1C_ROOT", "/opt/activitywatch/clickhouse-1c"))
    configured = os.getenv("AW_1C_MANAGER_BRIEF_STATE_DIR")
    if configured:
        return Path(configured)
    return root / "state" / "manager-brief"


def weekly_digest_state_dir() -> Path:
    root = Path(os.getenv("AW_1C_ROOT", "/opt/activitywatch/clickhouse-1c"))
    configured = os.getenv("AW_1C_WEEKLY_DIGEST_STATE_DIR")
    if configured:
        return Path(configured)
    return root / "state" / "weekly-digest"


def recovery_brief_state_dir() -> Path:
    root = Path(os.getenv("AW_1C_ROOT", "/opt/activitywatch/clickhouse-1c"))
    configured = os.getenv("AW_1C_RECOVERY_BRIEF_STATE_DIR")
    if configured:
        return Path(configured)
    return root / "state" / "recovery-brief"


def load_latest_manager_brief() -> dict[str, Any]:
    latest_path = manager_brief_state_dir() / "latest.json"
    if not latest_path.exists():
        raise HTTPException(status_code=404, detail="manager brief not generated yet")
    return json.loads(latest_path.read_text(encoding="utf-8"))


def load_latest_weekly_digest() -> dict[str, Any]:
    latest_path = weekly_digest_state_dir() / "latest.json"
    if not latest_path.exists():
        raise HTTPException(status_code=404, detail="weekly digest not generated yet")
    return json.loads(latest_path.read_text(encoding="utf-8"))


def load_latest_recovery_brief() -> dict[str, Any]:
    latest_path = recovery_brief_state_dir() / "latest.json"
    if not latest_path.exists():
        raise HTTPException(status_code=404, detail="recovery brief not generated yet")
    return json.loads(latest_path.read_text(encoding="utf-8"))


def load_brief_history_records(limit: int = 20) -> list[dict[str, Any]]:
    history_dir = manager_brief_state_dir() / "history"
    if not history_dir.exists():
        return []
    items: list[dict[str, Any]] = []
    for path in sorted(history_dir.glob("*.json"), reverse=True)[:limit]:
        payload = json.loads(path.read_text(encoding="utf-8"))
        items.append(
            {
                "generated_at": payload.get("generated_at"),
                "render_mode": payload.get("render_mode"),
                "model": payload.get("model"),
                "headline": payload.get("brief", {}).get("headline", ""),
                "path": path.name,
                "brief": payload.get("brief", {}),
            }
        )
    return items


def load_brief_history_payloads(limit: int = 200) -> list[dict[str, Any]]:
    history_dir = manager_brief_state_dir() / "history"
    if not history_dir.exists():
        return []
    items: list[dict[str, Any]] = []
    for path in sorted(history_dir.glob("*.json"), reverse=True)[:limit]:
        payload = json.loads(path.read_text(encoding="utf-8"))
        payload["_path"] = path.name
        items.append(payload)
    return items


def load_brief_history_record(name: str) -> dict[str, Any]:
    safe_name = Path(name).name
    if not safe_name.endswith(".json"):
        safe_name += ".json"
    path = manager_brief_state_dir() / "history" / safe_name
    if not path.exists():
        raise HTTPException(status_code=404, detail="manager brief history record not found")
    return json.loads(path.read_text(encoding="utf-8"))


def extract_delta(payload: dict[str, Any]) -> dict[str, Any]:
    delta = payload.get("context", {}).get("delta")
    if isinstance(delta, dict):
        return delta
    return {"available": False, "reason": "delta not present in artifact"}


def priority_score_for_change(item: dict[str, Any]) -> float:
    if item.get("priority_score") is not None:
        try:
            return float(item.get("priority_score"))
        except (TypeError, ValueError):
            return 0.0
    score = 0.0
    score += max(float(item.get("significance") or 0), 0)
    score += max(float(item.get("score_delta") or 0), 0) * 0.8
    score += max(float(item.get("open_cases_delta") or 0), 0) * 8
    score += max(float(item.get("detections_delta") or 0), 0) * 5
    score += max(float(item.get("active_locks_delta") or 0), 0) * 10
    return round(score, 2)


def build_weekly_trend_report(payloads: list[dict[str, Any]], days: int = 7) -> dict[str, Any]:
    now = datetime.now(UTC)
    period_start = now.date().toordinal() - max(days - 1, 0)
    filtered: list[dict[str, Any]] = []
    for payload in payloads:
        generated_at_raw = payload.get("generated_at")
        if not generated_at_raw:
            continue
        try:
            generated_at = datetime.fromisoformat(str(generated_at_raw))
        except ValueError:
            continue
        if generated_at.tzinfo is None:
            generated_at = generated_at.replace(tzinfo=UTC)
        if generated_at.date().toordinal() < period_start:
            continue
        payload["_generated_dt"] = generated_at
        filtered.append(payload)

    by_day: dict[str, dict[str, Any]] = {}
    for payload in filtered:
        key = payload["_generated_dt"].date().isoformat()
        previous = by_day.get(key)
        if previous is None or payload["_generated_dt"] > previous["_generated_dt"]:
            by_day[key] = payload

    daily_rows: list[dict[str, Any]] = []
    previous_summary: dict[str, Any] | None = None
    for key in sorted(by_day.keys()):
        payload = by_day[key]
        summary = payload.get("context", {}).get("portfolio_summary", {})
        row = {
            "date": key,
            "generated_at": payload.get("generated_at"),
            "companies_total": int(summary.get("companies_total", 0) or 0),
            "critical_total": int(summary.get("critical_total", 0) or 0),
            "high_total": int(summary.get("high_total", 0) or 0),
            "busy_total": int(summary.get("busy_total", 0) or 0),
            "open_cases_total": int(summary.get("open_cases_total", 0) or 0),
            "detections_total": int(summary.get("detections_total", 0) or 0),
            "activity_30d_total": float(summary.get("activity_30d_total", 0) or 0),
            "activity_forecast_30d_total": float(summary.get("activity_forecast_30d_total", 0) or 0),
        }
        if previous_summary:
            row["critical_delta_vs_prev_day"] = row["critical_total"] - int(previous_summary.get("critical_total", 0) or 0)
            row["busy_delta_vs_prev_day"] = row["busy_total"] - int(previous_summary.get("busy_total", 0) or 0)
            row["open_cases_delta_vs_prev_day"] = row["open_cases_total"] - int(previous_summary.get("open_cases_total", 0) or 0)
            row["detections_delta_vs_prev_day"] = row["detections_total"] - int(previous_summary.get("detections_total", 0) or 0)
        else:
            row["critical_delta_vs_prev_day"] = 0
            row["busy_delta_vs_prev_day"] = 0
            row["open_cases_delta_vs_prev_day"] = 0
            row["detections_delta_vs_prev_day"] = 0
        daily_rows.append(row)
        previous_summary = summary

    weekly_changes: dict[tuple[str, str], dict[str, Any]] = {}
    for payload in filtered:
        delta = extract_delta(payload)
        for item in delta.get("top_changes", []):
            key = (str(item.get("infobase") or ""), str(item.get("company") or ""))
            candidate = dict(item)
            candidate["generated_at"] = payload.get("generated_at")
            candidate["priority_score"] = priority_score_for_change(candidate)
            existing = weekly_changes.get(key)
            if existing is None or float(candidate["priority_score"]) > float(existing.get("priority_score") or 0):
                weekly_changes[key] = candidate

    weekly_top_changes = sorted(
        weekly_changes.values(),
        key=lambda item: (
            float(item.get("priority_score") or 0),
            float(item.get("significance") or 0),
            int(item.get("open_cases_delta") or 0),
        ),
        reverse=True,
    )[:20]

    latest = daily_rows[-1] if daily_rows else None
    return {
        "days": days,
        "period_start": datetime.fromordinal(period_start).date().isoformat(),
        "period_end": now.date().isoformat(),
        "daily": daily_rows,
        "latest": latest,
        "top_weekly_changes": weekly_top_changes,
    }


def tier_rank(value: str | None) -> int:
    return {
        "low": 1,
        "medium": 2,
        "high": 3,
        "critical": 4,
    }.get((value or "low").lower(), 1)


def find_change_for_company(
    items: list[dict[str, Any]],
    counterparty: str,
    infobase: str | None = None,
) -> dict[str, Any] | None:
    fallback: dict[str, Any] | None = None
    for item in items:
        if str(item.get("company") or "") != counterparty:
            continue
        if infobase and str(item.get("infobase") or "") == infobase:
            return item
        if fallback is None:
            fallback = item
    return fallback


def build_company_priority_context(summary_payload: dict[str, Any], infobase: str | None = None) -> dict[str, Any]:
    card = summary_payload.get("card") or {}
    counterparty = str(card.get("counterparty") or "")
    current_infobase = infobase or str(card.get("infobase") or "")
    latest_change = None
    weekly_change = None
    weekly_report = {"top_weekly_changes": []}

    try:
        latest_brief = load_latest_manager_brief()
        latest_change = find_change_for_company(
            extract_delta(latest_brief).get("top_changes", []),
            counterparty,
            current_infobase or None,
        )
    except HTTPException:
        latest_brief = None

    try:
        weekly_report = build_weekly_trend_report(load_brief_history_payloads(limit=400), days=7)
        weekly_change = find_change_for_company(
            weekly_report.get("top_weekly_changes", []),
            counterparty,
            current_infobase or None,
        )
    except Exception:
        weekly_report = {"top_weekly_changes": []}

    current_tier = "low"
    current_score = 0.0
    current_reason = "сильных weekly/delta-триггеров не видно"
    if latest_change:
        current_tier = str(latest_change.get("priority_tier") or "low")
        current_score = float(latest_change.get("priority_score") or 0)
        current_reason = str(latest_change.get("priority_reason") or latest_change.get("summary") or current_reason)
    elif weekly_change:
        current_tier = str(weekly_change.get("priority_tier") or "low")
        current_score = float(weekly_change.get("priority_score") or 0)
        current_reason = str(weekly_change.get("priority_reason") or weekly_change.get("summary") or current_reason)
    else:
        severity = str(card.get("signal_severity") or "low")
        current_tier = severity if severity in {"critical", "high", "medium", "low"} else "low"
        current_score = float(card.get("signal_score") or 0)
        if int(card.get("open_cases_total") or 0) > 0 or int(card.get("active_locks") or 0) > 0:
            current_reason = "у компании уже есть operational pressure: кейсы/блокировки/сигналы"

    evidence: list[str] = []
    actions: list[str] = []
    if latest_change:
        evidence.append(f"Последний запуск: {latest_change.get('summary') or current_reason}")
        if int(latest_change.get("open_cases_delta") or 0) > 0:
            evidence.append(f"Кейсы выросли на +{int(latest_change.get('open_cases_delta') or 0)}.")
            actions.append("Сразу разобрать новые открытые кейсы по компании.")
        if int(latest_change.get("detections_delta") or 0) > 0:
            evidence.append(f"Detections выросли на +{int(latest_change.get('detections_delta') or 0)}.")
        if int(latest_change.get("active_locks_delta") or 0) > 0:
            evidence.append(f"Блокировки выросли на +{int(latest_change.get('active_locks_delta') or 0)}.")
            actions.append("Проверить, не застряла ли файловая база в busy/lock-контуре.")
        if float(latest_change.get("forecast_delta") or 0) < 0:
            evidence.append(f"Прогноз 30д просел на {fmt_number(latest_change.get('forecast_delta'))}.")
    if weekly_change and weekly_change is not latest_change:
        evidence.append(f"За неделю компания вошла в top changes: {weekly_change.get('priority_reason') or weekly_change.get('summary')}.")
    if str(card.get("signal_severity") or "none") in {"critical", "high"}:
        evidence.append(f"Текущая severity {card.get('signal_severity')} при score {fmt_number(card.get('signal_score'))}.")
    if card.get("registry_match_mode") == "manual":
        evidence.append("Привязка к реестру manual: интерпретацию владельца и юр. соответствия проверять отдельно.")
    if int(card.get("open_cases_total") or 0) > 0:
        evidence.append(f"Сейчас открытых кейсов {int(card.get('open_cases_total') or 0)}.")
    if int(card.get("detections_total") or 0) > 0:
        evidence.append(f"Сейчас active detections {int(card.get('detections_total') or 0)}.")
    if int(card.get("active_locks") or 0) > 0:
        evidence.append(f"Сейчас активных блокировок {int(card.get('active_locks') or 0)}.")
    if int(card.get("days_since_last_activity") or 0) >= 7:
        evidence.append(f"Компания молчит уже {int(card.get('days_since_last_activity') or 0)} дн.")
        actions.append("Подтвердить, это плановая бизнес-пауза или выпадение из операционного ритма.")
    if not actions:
        actions.append("Сначала проверить последние сигналы и timeline компании, затем подтверждать бизнес-выводы.")
    if int(card.get("open_cases_total") or 0) > 0 and "Сразу разобрать новые открытые кейсы по компании." not in actions:
        actions.append("Разобрать открытые кейсы до обсуждения прогноза.")
    if int(card.get("active_locks") or 0) > 0 and "Проверить, не застряла ли файловая база в busy/lock-контуре." not in actions:
        actions.append("Проверить busy/lock-контур файловой базы.")

    if tier_rank(current_tier) >= tier_rank("high"):
        verdict = (
            "Приоритет действительно высокий не из-за общей красноты портфеля, а из-за конкретных operational-сдвигов по этой компании."
        )
    elif tier_rank(current_tier) == tier_rank("medium"):
        verdict = "Приоритет средний: явный сдвиг есть, но он ещё не выглядит аварийным."
    else:
        verdict = "Приоритет низкий: компания видна в портфеле, но жёсткого триггера на разбор первой очереди нет."

    return {
        "current_priority_tier": current_tier,
        "current_priority_score": round(current_score, 2),
        "current_priority_reason": current_reason,
        "latest_change": latest_change,
        "weekly_change": weekly_change,
        "weekly_period_start": weekly_report.get("period_start"),
        "weekly_period_end": weekly_report.get("period_end"),
        "verdict": verdict,
        "evidence": evidence[:8],
        "actions": actions[:5],
    }


def find_recovery_incident_for_company(
    items: list[dict[str, Any]],
    counterparty: str,
    infobase: str | None = None,
) -> dict[str, Any] | None:
    fallback: dict[str, Any] | None = None
    for item in items:
        if str(item.get("company") or "") != counterparty:
            continue
        if infobase and str(item.get("infobase") or "") == infobase:
            return item
        if fallback is None:
            fallback = item
    return fallback


def build_company_recovery_context(summary_payload: dict[str, Any], infobase: str | None = None) -> dict[str, Any]:
    card = summary_payload.get("card") or {}
    priority_context = summary_payload.get("priority_context") or build_company_priority_context(summary_payload, infobase)
    counterparty = str(card.get("counterparty") or "")
    matched_incident = None
    recovery_generated_at = None

    try:
        recovery_payload = load_latest_recovery_brief()
        matched_incident = find_recovery_incident_for_company(
            recovery_payload.get("recovery", {}).get("top_incidents", []),
            counterparty,
            infobase or str(card.get("infobase") or "") or None,
        )
        recovery_generated_at = recovery_payload.get("generated_at")
    except HTTPException:
        recovery_payload = None

    if matched_incident:
        diagnosis = str(matched_incident.get("diagnosis") or priority_context.get("current_priority_reason") or "-")
        actions = [str(item) for item in matched_incident.get("actions", []) if str(item).strip()]
        stop_doing = str(matched_incident.get("stop_doing") or "")
        target_state = str(matched_incident.get("target_state_24h") or "")
        confidence = "recovery-brief/codex"
    else:
        diagnosis_parts = [str(priority_context.get("current_priority_reason") or "").strip()]
        if int(card.get("open_cases_total") or 0) > 0:
            diagnosis_parts.append(f"открытых кейсов {int(card.get('open_cases_total') or 0)}")
        if int(card.get("active_locks") or 0) > 0:
            diagnosis_parts.append(f"активных блокировок {int(card.get('active_locks') or 0)}")
        if int(card.get("detections_total") or 0) > 0:
            diagnosis_parts.append(f"detections {int(card.get('detections_total') or 0)}")
        diagnosis = "; ".join(part for part in diagnosis_parts if part) or "явного recovery-диагноза пока нет"
        actions = list(priority_context.get("actions", []))
        if int(card.get("open_cases_total") or 0) > 0 and "Назначить владельца на закрытие открытых кейсов в течение 24 часов." not in actions:
            actions.insert(0, "Назначить владельца на закрытие открытых кейсов в течение 24 часов.")
        if int(card.get("active_locks") or 0) > 0 and "Снять busy/lock-контур прежде чем обсуждать долгосрочный прогноз." not in actions:
            actions.append("Снять busy/lock-контур прежде чем обсуждать долгосрочный прогноз.")
        if card.get("registry_match_mode") == "manual" and "Проверить корректность manual-сопоставления до жёстких управленческих выводов." not in actions:
            actions.append("Проверить корректность manual-сопоставления до жёстких управленческих выводов.")
        stop_doing = "Не проводить общие обсуждения без владельца, срока и числовой цели на день."
        target_cases = max(int(card.get("open_cases_total") or 0) - 3, 0)
        target_state = (
            f"Снизить открытые кейсы ниже {target_cases}, "
            f"снять новый прирост и подтвердить отсутствие лишних блокировок по следующему запуску."
        )
        confidence = "deterministic-fallback"

    return {
        "generated_at": recovery_generated_at,
        "confidence": confidence,
        "diagnosis": diagnosis,
        "actions": actions[:5],
        "stop_doing": stop_doing,
        "target_state_24h": target_state,
        "source_incident": matched_incident,
    }


def grafana_company_dashboard_url() -> str:
    return os.getenv(
        "AW_1C_MANAGER_BRIEF_GRAFANA_URL",
        "http://192.0.2.11:3000/d/1c-file-mgmt/1c-file-management-board",
    )


def resolve_company_portfolio_card(company_ref: str, infobase: str | None = None) -> dict[str, Any]:
    client = ch_client()
    filters = [
        "("
        + " OR ".join(
            [
                f"company_entity_key = {q(company_ref)}",
                f"counterparty = {q(company_ref)}",
                f"company_name = {q(company_ref)}",
                f"source_counterparty = {q(company_ref)}",
            ]
        )
        + ")"
    ]
    if infobase:
        filters.append(f"infobase = {q(infobase)}")
    sql = f"""
    SELECT *
    FROM analytics_1c.v_company_portfolio_overview
    WHERE {' AND '.join(filters)}
    ORDER BY last_company_snapshot_at DESC, amount_30d DESC
    LIMIT 1
    """
    rows = rows_to_dict(client.query(sql))
    if not rows:
        raise HTTPException(status_code=404, detail="company not found in analytics_1c.v_company_portfolio_overview")
    return rows[0]


def fmt_number(value: Any) -> str:
    if value is None or value == "":
        return "-"
    if isinstance(value, float):
        return f"{value:,.2f}".replace(",", " ").replace(".", ",")
    if isinstance(value, int):
        return f"{value:,}".replace(",", " ")
    return str(value)


def severity_badge(severity: str) -> str:
    tone = {
        "critical": "critical",
        "high": "high",
        "medium": "medium",
        "low": "low",
        "none": "none",
    }.get((severity or "").lower(), "none")
    return f'<span class="badge badge-{tone}">{html.escape(severity or "none")}</span>'


def company_detail_url(company_ref: str, infobase: str | None = None) -> str:
    base = f"/manager/company/{quote(company_ref)}"
    if infobase:
        return f"{base}?infobase={quote(infobase)}"
    return base


def manager_brief_history_html_url(name: str) -> str:
    safe_name = Path(name).name
    return f"/manager/briefs/{quote(safe_name)}"


def render_manager_brief_html(payload: dict[str, Any]) -> str:
    brief = payload.get("brief", {})
    context = payload.get("context", {})
    summary = context.get("portfolio_summary", {})
    freshness = context.get("freshness", [])
    manager_questions = brief.get("manager_questions", [])
    management_plan = brief.get("management_plan", [])
    top_risks = brief.get("top_risks", [])
    top_forecasts = brief.get("top_forecasts", [])
    actions = brief.get("actions", [])
    caveats = brief.get("caveats", [])
    render_mode = payload.get("render_mode", "unknown")
    generated_at = payload.get("generated_at", "")
    history_url = "/api/1/analytics-1c/manager/brief/history"
    history_html_url = "/manager/briefs"
    delta_html_url = "/manager/changes"
    weekly_html_url = "/manager/trends/weekly"
    weekly_digest_url = "/manager/digest/weekly"
    recovery_html_url = "/manager/recovery"
    actions_html_url = "/manager/actions"
    problematic_1d_url = "/manager/problematic?days=1"
    problematic_7d_url = "/manager/problematic?days=7"
    json_url = "/api/1/analytics-1c/manager/brief/latest"
    md_url = "/api/1/analytics-1c/manager/brief/latest.md"
    grafana_url = grafana_company_dashboard_url()

    freshness_rows = []
    for item in freshness:
        status = "stale" if item.get("stale") else "fresh"
        freshness_rows.append(
            "<tr>"
            f"<td>{html.escape(str(item.get('source', '')))}</td>"
            f"<td>{html.escape(str(item.get('latest_ts', '-')))}</td>"
            f"<td>{html.escape(str(item.get('lag_hours', '-')))}</td>"
            f"<td><span class=\"freshness freshness-{status}\">{'просрочен' if status == 'stale' else 'свежий'}</span></td>"
            "</tr>"
        )

    risk_cards = []
    for item in top_risks:
        risk_cards.append(
            "<article class=\"stack-card\">"
            f"<div class=\"stack-card-head\"><h3>{html.escape(item.get('company', '-'))}</h3>{severity_badge(item.get('severity', ''))}</div>"
            f"<p class=\"stack-card-body\">{html.escape(item.get('reason', '-'))}</p>"
            f"<p class=\"stack-card-action\"><strong>Действие:</strong> {html.escape(item.get('recommended_action', '-'))}</p>"
            f"<p class=\"stack-card-action\"><a class=\"inline-link\" href=\"{company_detail_url(str(item.get('company_entity_key') or item.get('company') or '-'), str(item.get('infobase') or '') or None)}\">Открыть карточку компании</a></p>"
            "</article>"
        )

    forecast_cards = []
    for item in top_forecasts:
        company = item.get("company", "-")
        forecast_cards.append(
            "<article class=\"stack-card\">"
            f"<div class=\"stack-card-head\"><h3>{html.escape(company)}</h3></div>"
            f"<p class=\"metric-line\"><strong>Прогноз 30д:</strong> {html.escape(item.get('forecast_30d', '-'))}</p>"
            f"<p class=\"stack-card-body\">{html.escape(item.get('interpretation', '-'))}</p>"
            f"<a class=\"inline-link\" href=\"{company_detail_url(str(item.get('company_entity_key') or company), str(item.get('infobase') or '') or None)}\">Карточка компании</a>"
            "</article>"
        )

    question_cards = []
    for item in manager_questions:
        question_cards.append(
            "<article class=\"stack-card\">"
            f"<div class=\"stack-card-head\"><h3>{html.escape(item.get('question', '-'))}</h3></div>"
            f"<p class=\"stack-card-body\">{html.escape(item.get('answer', '-'))}</p>"
            f"<p class=\"stack-card-action\"><strong>Рекомендация:</strong> {html.escape(item.get('recommended_action', '-'))}</p>"
            "</article>"
        )

    horizon_label = {"today": "Сегодня", "week": "Неделя", "30d": "30 дней"}
    plan_cards = []
    for item in management_plan:
        plan_cards.append(
            "<article class=\"stack-card\">"
            f"<div class=\"stack-card-head\"><h3>{html.escape(item.get('focus', '-'))}</h3><span class=\"freshness freshness-fresh\">{html.escape(horizon_label.get(str(item.get('horizon')), str(item.get('horizon', '-'))))}</span></div>"
            f"<p class=\"stack-card-body\"><strong>Действие:</strong> {html.escape(item.get('action', '-'))}</p>"
            f"<p class=\"stack-card-action\"><strong>Ожидаемый эффект:</strong> {html.escape(item.get('expected_effect', '-'))}</p>"
            f"<p class=\"stack-card-action\"><strong>Метрика:</strong> {html.escape(item.get('metric', '-'))}</p>"
            "</article>"
        )

    summary_items = "\n".join(f"<li>{html.escape(str(item))}</li>" for item in brief.get("summary", []))
    action_items = "\n".join(f"<li>{html.escape(str(item))}</li>" for item in actions)
    caveat_items = "\n".join(f"<li>{html.escape(str(item))}</li>" for item in caveats)
    morning_regulation_items = "\n".join(
        f"<li>{html.escape(str(item))}</li>"
        for item in [
            "Открыть brief, changes и weekly digest. Сначала понять: хвост кейсов растёт или сокращается.",
            "Выбрать не больше 5 компаний первой очереди. Критерии: рост open cases, active locks, manual-match, повторный critical без улучшения.",
            "По каждой компании требовать только 4 вещи: причина, владелец, срок, измеримый результат к вечеру.",
            "Не принимать формулировки «смотрим», «разбираемся», «в работе». Принимать только конкретный план закрытия.",
        ]
    )
    evening_regulation_items = "\n".join(
        f"<li>{html.escape(str(item))}</li>"
        for item in [
            "По каждой компании дня спросить только факт: сколько кейсов было утром и сколько стало вечером.",
            "Отдельно проверить, снялись ли блокировки и исчез ли прирост новых кейсов.",
            "По каждой компании зафиксировать один статус: снято, частично снято, не снято.",
            "Если результата нет, менять владельца или усиливать контроль; не переносить хвост молча.",
        ]
    )
    hard_rules_items = "\n".join(
        f"<li>{html.escape(str(item))}</li>"
        for item in [
            "Не пытаться разбирать все 41 компании сразу.",
            "Не путать operational critical с финансовым крахом портфеля.",
            "Не делать жёстких оргвыводов по manual-match компаниям без проверки соответствия реестру.",
            "Главный KPI на сейчас: закрытые кейсы и снятые блокировки, а не количество проведённых разборов.",
        ]
    )

    return f"""<!doctype html>
<html lang="ru">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta http-equiv="refresh" content="300">
  <title>1C Executive Brief</title>
  <style>
    :root {{
      --bg: #f4f1ea;
      --paper: #fffdf8;
      --ink: #1c1a17;
      --muted: #6b655c;
      --line: #d8d0c4;
      --accent: #005f73;
      --accent-soft: #d9eef2;
      --critical: #9b2226;
      --high: #bb3e03;
      --medium: #ca6702;
      --low: #4d7c0f;
      --none: #687076;
      --fresh: #1d6f42;
      --stale: #9b2226;
      --shadow: 0 14px 40px rgba(28, 26, 23, 0.08);
    }}
    * {{ box-sizing: border-box; }}
    body {{
      margin: 0;
      font-family: \"IBM Plex Sans\", \"Segoe UI\", system-ui, sans-serif;
      background:
        radial-gradient(circle at top left, rgba(0, 95, 115, 0.08), transparent 28%),
        linear-gradient(180deg, #faf7f2 0%, var(--bg) 100%);
      color: var(--ink);
    }}
    .shell {{
      max-width: 1380px;
      margin: 0 auto;
      padding: 28px 22px 60px;
    }}
    .hero {{
      background: linear-gradient(135deg, rgba(0,95,115,0.94), rgba(10,77,104,0.92));
      color: #f8fbfc;
      border-radius: 24px;
      padding: 28px 30px;
      box-shadow: var(--shadow);
    }}
    .hero-top {{
      display: flex;
      justify-content: space-between;
      gap: 20px;
      align-items: flex-start;
      flex-wrap: wrap;
    }}
    .hero h1 {{
      margin: 0 0 12px;
      font-size: clamp(28px, 4vw, 44px);
      line-height: 1.05;
    }}
    .hero-meta {{
      color: rgba(248, 251, 252, 0.82);
      font-size: 14px;
    }}
    .hero-links {{
      display: flex;
      gap: 10px;
      flex-wrap: wrap;
    }}
    .hero-links a {{
      text-decoration: none;
      color: #f8fbfc;
      border: 1px solid rgba(248, 251, 252, 0.28);
      padding: 9px 12px;
      border-radius: 999px;
      font-size: 14px;
      backdrop-filter: blur(4px);
    }}
    .hero-links a:hover {{
      background: rgba(248, 251, 252, 0.12);
    }}
    .grid {{
      display: grid;
      grid-template-columns: repeat(12, minmax(0, 1fr));
      gap: 18px;
      margin-top: 22px;
    }}
    .panel {{
      background: var(--paper);
      border: 1px solid var(--line);
      border-radius: 22px;
      padding: 22px;
      box-shadow: var(--shadow);
    }}
    .panel h2 {{
      margin: 0 0 14px;
      font-size: 21px;
      line-height: 1.1;
    }}
    .panel h3 {{
      margin: 0;
      font-size: 18px;
      line-height: 1.2;
    }}
    .span-12 {{ grid-column: span 12; }}
    .span-8 {{ grid-column: span 8; }}
    .span-6 {{ grid-column: span 6; }}
    .span-4 {{ grid-column: span 4; }}
    .span-3 {{ grid-column: span 3; }}
    .summary-list,
    .action-list,
    .caveat-list {{
      margin: 0;
      padding-left: 20px;
      line-height: 1.55;
    }}
    .stats {{
      display: grid;
      grid-template-columns: repeat(4, minmax(0, 1fr));
      gap: 12px;
    }}
    .stat {{
      padding: 16px;
      border-radius: 18px;
      background: linear-gradient(180deg, #fff 0%, #f7f4ee 100%);
      border: 1px solid var(--line);
    }}
    .stat-label {{
      color: var(--muted);
      font-size: 13px;
      margin-bottom: 8px;
    }}
    .stat-value {{
      font-size: 26px;
      font-weight: 700;
      letter-spacing: -0.03em;
    }}
    .stack {{
      display: grid;
      gap: 14px;
    }}
    .stack-card {{
      border: 1px solid var(--line);
      border-radius: 18px;
      padding: 16px 18px;
      background: #fffdfa;
    }}
    .stack-card-head {{
      display: flex;
      justify-content: space-between;
      align-items: center;
      gap: 12px;
      margin-bottom: 10px;
    }}
    .stack-card-body,
    .stack-card-action,
    .metric-line {{
      margin: 0;
      line-height: 1.5;
    }}
    .stack-card-action {{
      margin-top: 10px;
      color: var(--muted);
    }}
    .badge {{
      display: inline-flex;
      align-items: center;
      justify-content: center;
      padding: 6px 10px;
      border-radius: 999px;
      font-size: 12px;
      font-weight: 700;
      text-transform: uppercase;
      letter-spacing: 0.06em;
      white-space: nowrap;
      color: #fff;
    }}
    .badge-critical {{ background: var(--critical); }}
    .badge-high {{ background: var(--high); }}
    .badge-medium {{ background: var(--medium); }}
    .badge-low {{ background: var(--low); }}
    .badge-none {{ background: var(--none); }}
    .freshness {{
      display: inline-flex;
      border-radius: 999px;
      padding: 4px 9px;
      font-size: 12px;
      font-weight: 700;
      text-transform: uppercase;
      letter-spacing: 0.04em;
    }}
    .freshness-fresh {{
      background: rgba(29, 111, 66, 0.12);
      color: var(--fresh);
    }}
    .freshness-stale {{
      background: rgba(155, 34, 38, 0.12);
      color: var(--stale);
    }}
    table {{
      width: 100%;
      border-collapse: collapse;
      font-size: 14px;
    }}
    th, td {{
      text-align: left;
      padding: 10px 8px;
      border-bottom: 1px solid var(--line);
      vertical-align: top;
    }}
    th {{
      color: var(--muted);
      font-weight: 600;
      font-size: 12px;
      text-transform: uppercase;
      letter-spacing: 0.06em;
    }}
    .inline-link {{
      color: var(--accent);
      text-decoration: none;
      font-weight: 600;
    }}
    .inline-link:hover {{
      text-decoration: underline;
    }}
    @media (max-width: 1100px) {{
      .span-8, .span-6, .span-4, .span-3 {{ grid-column: span 12; }}
      .stats {{ grid-template-columns: repeat(2, minmax(0, 1fr)); }}
    }}
    @media (max-width: 640px) {{
      .shell {{ padding: 16px 14px 40px; }}
      .hero {{ padding: 22px 18px; }}
      .stats {{ grid-template-columns: 1fr; }}
      .stack-card-head {{ flex-direction: column; align-items: flex-start; }}
      th:nth-child(2), td:nth-child(2),
      th:nth-child(3), td:nth-child(3) {{
        display: none;
      }}
    }}
  </style>
</head>
<body>
  <main class="shell">
    <section class="hero">
      <div class="hero-top">
        <div>
          <div class="hero-meta">AW-rus · 1C Executive Brief · render mode: {html.escape(render_mode)}</div>
          <h1>{html.escape(brief.get("headline", "Executive brief недоступен"))}</h1>
          <div class="hero-meta">Сформировано: {html.escape(generated_at)}</div>
        </div>
        <nav class="hero-links">
          <a href="{html.escape(json_url)}">JSON</a>
          <a href="{html.escape(md_url)}">Markdown</a>
          <a href="{html.escape(history_html_url)}">История brief</a>
          <a href="{html.escape(delta_html_url)}">Что изменилось</a>
          <a href="{html.escape(weekly_html_url)}">Неделя</a>
          <a href="{html.escape(weekly_digest_url)}">Weekly digest</a>
          <a href="{html.escape(recovery_html_url)}">AI recovery</a>
          <a href="{html.escape(actions_html_url)}">Очередь действий</a>
          <a href="{html.escape(problematic_1d_url)}">Проблемные 1д</a>
          <a href="{html.escape(problematic_7d_url)}">Проблемные 7д</a>
          <a href="{html.escape(history_url)}">History API</a>
          <a href="{html.escape(grafana_url)}">Grafana</a>
        </nav>
      </div>
    </section>

    <section class="grid">
      <article class="panel span-8">
        <h2>Краткий комментарий</h2>
        <ol class="summary-list">
          {summary_items}
        </ol>
      </article>
      <article class="panel span-4">
        <h2>Портфель</h2>
        <div class="stats">
          <div class="stat">
            <div class="stat-label">Компаний</div>
            <div class="stat-value">{fmt_number(summary.get("companies_total"))}</div>
          </div>
          <div class="stat">
            <div class="stat-label">Critical</div>
            <div class="stat-value">{fmt_number(summary.get("critical_total"))}</div>
          </div>
          <div class="stat">
            <div class="stat-label">Кейсы</div>
            <div class="stat-value">{fmt_number(summary.get("open_cases_total"))}</div>
          </div>
          <div class="stat">
            <div class="stat-label">Прогноз 30д</div>
            <div class="stat-value">{fmt_number(summary.get("activity_forecast_30d_total"))}</div>
          </div>
        </div>
      </article>

      <article class="panel span-12">
        <h2>Простые ответы для руководителя</h2>
        <div class="stack">
          {''.join(question_cards) or '<p>Нет данных.</p>'}
        </div>
      </article>

      <article class="panel span-12">
        <h2>План действий руководителя</h2>
        <div class="stack">
          {''.join(plan_cards) or '<p>Нет данных.</p>'}
        </div>
      </article>

      <article class="panel span-6">
        <h2>Компании риска</h2>
        <div class="stack">
          {''.join(risk_cards) or '<p>Нет данных.</p>'}
        </div>
      </article>

      <article class="panel span-6">
        <h2>Прогноз активности 30 дней</h2>
        <div class="stack">
          {''.join(forecast_cards) or '<p>Нет данных.</p>'}
        </div>
      </article>

      <article class="panel span-6">
        <h2>Что проверить руководителю</h2>
        <ul class="action-list">
          {action_items}
        </ul>
      </article>

      <article class="panel span-6">
        <h2>Ограничения интерпретации</h2>
        <ul class="caveat-list">
          {caveat_items}
        </ul>
      </article>

      <article class="panel span-6">
        <h2>Регламент руководителя: утро</h2>
        <ul class="action-list">
          {morning_regulation_items}
        </ul>
      </article>

      <article class="panel span-6">
        <h2>Регламент руководителя: вечер</h2>
        <ul class="action-list">
          {evening_regulation_items}
        </ul>
      </article>

      <article class="panel span-12">
        <h2>Жёсткие правила управления</h2>
        <ul class="action-list">
          {hard_rules_items}
        </ul>
      </article>

      <article class="panel span-12">
        <h2>Свежесть источников</h2>
        <table>
          <thead>
            <tr>
              <th>Источник</th>
              <th>Последняя отметка</th>
              <th>Отставание, ч</th>
              <th>Статус</th>
            </tr>
          </thead>
          <tbody>
            {''.join(freshness_rows)}
          </tbody>
        </table>
      </article>
    </section>
  </main>
</body>
</html>"""


def problematic_companies(days: int = 7, limit: int = 50) -> list[dict[str, Any]]:
    client = ch_client()
    sql = f"""
    WITH recent AS (
        SELECT
            infobase,
            counterparty AS company_entity_key,
            max(generated_at) AS latest_signal_at,
            max(score) AS max_score,
            sum(score) AS total_score,
            count() AS signals_total,
            countIf(severity = 'critical') AS critical_total,
            countIf(severity = 'high') AS high_total,
            argMax(severity, tuple(score, generated_at)) AS top_severity,
            argMax(signal_type, tuple(score, generated_at)) AS top_signal_type,
            argMax(summary, tuple(score, generated_at)) AS top_summary
        FROM analytics_1c.company_health_signals
        WHERE generated_at >= now() - INTERVAL {int(days)} DAY
        GROUP BY infobase, counterparty
    )
    SELECT
        p.infobase AS infobase,
        p.company_entity_key AS company_entity_key,
        p.counterparty AS counterparty,
        p.source_counterparty,
        p.company_name,
        p.normalized_counterparty,
        p.registry_match_mode,
        p.registry_assignee_name,
        p.registry_status,
        p.signal_severity,
        p.signal_score,
        p.amount_30d,
        p.amount_forecast_30d,
        p.current_status,
        p.active_locks,
        p.open_cases_total,
        p.detections_total,
        r.latest_signal_at,
        r.max_score,
        r.total_score,
        r.signals_total,
        r.critical_total,
        r.high_total,
        r.top_severity,
        r.top_signal_type,
        r.top_summary
    FROM recent AS r
    INNER JOIN analytics_1c.v_company_portfolio_overview AS p
        ON p.infobase = r.infobase
       AND p.company_entity_key = r.company_entity_key
    ORDER BY r.max_score DESC, r.signals_total DESC, p.amount_30d DESC, p.counterparty
    LIMIT {int(limit)}
    """
    items = rows_to_dict(client.query(sql))
    for item in items:
        if "infobase" not in item and "p.infobase" in item:
            item["infobase"] = item["p.infobase"]
        if "counterparty" not in item and "p.counterparty" in item:
            item["counterparty"] = item["p.counterparty"]
    return items


def management_actions(
    priority_tier: str | None = None,
    owner: str | None = None,
    limit: int = 100,
) -> list[dict[str, Any]]:
    client = ch_client()
    where: list[str] = []
    if priority_tier:
        where.append(f"priority_tier = {q(priority_tier)}")
    if owner:
        where.append(f"owner_name = {q(owner)}")
    sql = f"""
    SELECT
        generated_at,
        action_id,
        infobase,
        company_entity_key,
        counterparty,
        company_name,
        organization,
        owner_name,
        action_type,
        priority_tier,
        priority_score,
        priority_rank,
        deadline_hint,
        reason,
        recommended_action,
        evidence_summary,
        open_cases_total,
        detections_total,
        active_locks,
        days_since_last_activity,
        risky_changes_24h,
        large_adjustments_24h,
        max_large_adjustment_amount,
        last_seen_at
    FROM analytics_1c.v_company_management_actions_current
    {"WHERE " + " AND ".join(where) if where else ""}
    ORDER BY priority_rank DESC, priority_score DESC, counterparty, action_type
    LIMIT {int(limit)}
    """
    return rows_to_dict(client.query(sql))


def management_company_actions(
    priority_tier: str | None = None,
    owner: str | None = None,
    limit: int = 100,
) -> list[dict[str, Any]]:
    client = ch_client()
    where: list[str] = []
    if priority_tier:
        where.append(f"priority_tier = {q(priority_tier)}")
    if owner:
        where.append(f"owner_names ILIKE {q('%' + owner + '%')}")
    sql = f"""
    SELECT
        generated_at,
        infobase,
        company_entity_key,
        counterparty,
        company_name,
        organization,
        owner_names,
        priority_tier,
        priority_rank,
        priority_score,
        actions_total,
        critical_actions_total,
        high_actions_total,
        medium_actions_total,
        low_actions_total,
        action_types_summary,
        recommended_action_summary,
        reason_summary,
        evidence_summary,
        open_cases_total,
        detections_total,
        active_locks,
        days_since_last_activity,
        risky_changes_24h,
        large_adjustments_24h,
        max_large_adjustment_amount,
        last_seen_at
    FROM analytics_1c.v_company_management_companies_current
    {"WHERE " + " AND ".join(where) if where else ""}
    ORDER BY priority_rank DESC, priority_score DESC, counterparty
    LIMIT {int(limit)}
    """
    return rows_to_dict(client.query(sql))


def company_management_actions(company_ref: str, infobase: str | None = None, limit: int = 20) -> list[dict[str, Any]]:
    client = ch_client()
    card = resolve_company_portfolio_card(company_ref, infobase)
    filters = [f"company_entity_key = {q(str(card.get('company_entity_key') or company_ref))}"]
    if card.get("infobase") or infobase:
        filters.append(f"infobase = {q(str(card.get('infobase') or infobase))}")
    sql = f"""
    SELECT
        generated_at,
        action_id,
        infobase,
        company_entity_key,
        counterparty,
        company_name,
        organization,
        owner_name,
        action_type,
        priority_tier,
        priority_score,
        priority_rank,
        deadline_hint,
        reason,
        recommended_action,
        evidence_summary,
        open_cases_total,
        detections_total,
        active_locks,
        days_since_last_activity,
        risky_changes_24h,
        large_adjustments_24h,
        max_large_adjustment_amount,
        last_seen_at
    FROM analytics_1c.v_company_management_actions_current
    WHERE {' AND '.join(filters)}
    ORDER BY priority_rank DESC, priority_score DESC, action_type
    LIMIT {int(limit)}
    """
    return rows_to_dict(client.query(sql))


def _actions_query_suffix(priority_tier: str | None = None, owner: str | None = None) -> str:
    parts: list[str] = []
    if priority_tier:
        parts.append(f"priority_tier={quote(priority_tier)}")
    if owner:
        parts.append(f"owner={quote(owner)}")
    return ("?" + "&".join(parts)) if parts else ""


def render_brief_history_html(items: list[dict[str, Any]]) -> str:
    rows = []
    for item in items:
        rows.append(
            "<tr>"
            f"<td><a class=\"inline-link\" href=\"{manager_brief_history_html_url(str(item.get('path', '')))}\">{html.escape(str(item.get('generated_at', '-')))}</a></td>"
            f"<td>{html.escape(str(item.get('render_mode', '-')))}</td>"
            f"<td>{html.escape(str(item.get('model') or '-'))}</td>"
            f"<td>{html.escape(str(item.get('headline') or '-'))}</td>"
            f"<td><a class=\"inline-link\" href=\"/manager/briefs/{quote(str(item.get('path', '')))}/changes\">Изменения</a></td>"
            f"<td><a class=\"inline-link\" href=\"/api/1/analytics-1c/manager/brief/history/{quote(str(item.get('path', '')))}\">JSON</a></td>"
            "</tr>"
        )
    return f"""<!doctype html>
<html lang="ru">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta http-equiv="refresh" content="300">
  <title>1C Brief History</title>
  <style>
    :root {{
      --bg: #f4f1ea; --paper: #fffdf8; --ink: #1c1a17; --muted: #6b655c; --line: #d8d0c4; --accent: #005f73; --shadow: 0 14px 40px rgba(28, 26, 23, 0.08);
    }}
    * {{ box-sizing: border-box; }}
    body {{ margin: 0; font-family: "IBM Plex Sans", "Segoe UI", system-ui, sans-serif; background: linear-gradient(180deg, #faf7f2 0%, var(--bg) 100%); color: var(--ink); }}
    .shell {{ max-width: 1380px; margin: 0 auto; padding: 28px 22px 60px; }}
    .hero {{ background: linear-gradient(135deg, rgba(0,95,115,0.94), rgba(10,77,104,0.92)); color: #f8fbfc; border-radius: 24px; padding: 28px 30px; box-shadow: var(--shadow); }}
    .hero h1 {{ margin: 0 0 10px; font-size: clamp(28px, 4vw, 42px); }}
    .hero-links {{ display: flex; gap: 10px; flex-wrap: wrap; }}
    .hero-links a {{ text-decoration: none; color: #f8fbfc; border: 1px solid rgba(248, 251, 252, 0.28); padding: 9px 12px; border-radius: 999px; font-size: 14px; }}
    .panel {{ margin-top: 22px; background: var(--paper); border: 1px solid var(--line); border-radius: 22px; padding: 22px; box-shadow: var(--shadow); }}
    table {{ width: 100%; border-collapse: collapse; font-size: 14px; }}
    th, td {{ text-align: left; padding: 10px 8px; border-bottom: 1px solid var(--line); vertical-align: top; }}
    th {{ color: var(--muted); font-weight: 600; font-size: 12px; text-transform: uppercase; letter-spacing: 0.06em; }}
    .inline-link {{ color: var(--accent); text-decoration: none; font-weight: 600; }}
    .inline-link:hover {{ text-decoration: underline; }}
  </style>
</head>
<body>
  <main class="shell">
    <section class="hero">
      <h1>История executive brief</h1>
      <div class="hero-links">
        <a href="/manager/brief">Текущий brief</a>
        <a href="/manager/changes">Что изменилось</a>
        <a href="/manager/trends/weekly">Неделя</a>
        <a href="/manager/recovery">AI recovery</a>
        <a href="/manager/actions">Очередь действий</a>
        <a href="/manager/problematic?days=1">Проблемные 1д</a>
        <a href="/manager/problematic?days=7">Проблемные 7д</a>
      </div>
    </section>
    <section class="panel">
      <table>
        <thead>
          <tr>
            <th>Сформировано</th>
            <th>Режим</th>
            <th>Модель</th>
            <th>Headline</th>
            <th>Delta</th>
            <th>Raw</th>
          </tr>
        </thead>
        <tbody>
          {''.join(rows) or '<tr><td colspan="6">История пока пуста.</td></tr>'}
        </tbody>
      </table>
    </section>
  </main>
</body>
</html>"""


def render_problematic_companies_html(items: list[dict[str, Any]], days: int) -> str:
    rows = []
    for item in items:
        rows.append(
            "<tr>"
            f"<td><a class=\"inline-link\" href=\"{company_detail_url(str(item.get('company_entity_key') or item.get('counterparty') or '-'), str(item.get('infobase', '')) if item.get('infobase') else None)}\">{html.escape(str(item.get('counterparty', '-')))}</a></td>"
            f"<td>{html.escape(str(item.get('normalized_counterparty') or '-'))}</td>"
            f"<td>{severity_badge(str(item.get('top_severity') or item.get('signal_severity') or 'none'))}</td>"
            f"<td>{fmt_number(item.get('max_score'))}</td>"
            f"<td>{fmt_number(item.get('signals_total'))}</td>"
            f"<td>{fmt_number(item.get('critical_total'))}</td>"
            f"<td>{fmt_number(item.get('amount_30d'))}</td>"
            f"<td>{fmt_number(item.get('amount_forecast_30d'))}</td>"
            f"<td>{html.escape(str(item.get('top_signal_type') or '-'))}</td>"
            f"<td>{html.escape(str(item.get('top_summary') or '-'))}</td>"
            "</tr>"
        )
    return f"""<!doctype html>
<html lang="ru">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta http-equiv="refresh" content="300">
  <title>1C Problem Companies</title>
  <style>
    :root {{
      --bg: #f4f1ea; --paper: #fffdf8; --ink: #1c1a17; --muted: #6b655c; --line: #d8d0c4; --accent: #005f73; --shadow: 0 14px 40px rgba(28, 26, 23, 0.08);
      --critical: #9b2226; --high: #bb3e03; --medium: #ca6702; --low: #4d7c0f; --none: #687076;
    }}
    * {{ box-sizing: border-box; }}
    body {{ margin: 0; font-family: "IBM Plex Sans", "Segoe UI", system-ui, sans-serif; background: linear-gradient(180deg, #faf7f2 0%, var(--bg) 100%); color: var(--ink); }}
    .shell {{ max-width: 1500px; margin: 0 auto; padding: 28px 22px 60px; }}
    .hero {{ background: linear-gradient(135deg, rgba(0,95,115,0.94), rgba(10,77,104,0.92)); color: #f8fbfc; border-radius: 24px; padding: 28px 30px; box-shadow: var(--shadow); }}
    .hero h1 {{ margin: 0 0 10px; font-size: clamp(28px, 4vw, 42px); }}
    .hero p {{ margin: 0 0 14px; line-height: 1.5; color: rgba(248,251,252,0.88); }}
    .hero-links {{ display: flex; gap: 10px; flex-wrap: wrap; }}
    .hero-links a {{ text-decoration: none; color: #f8fbfc; border: 1px solid rgba(248, 251, 252, 0.28); padding: 9px 12px; border-radius: 999px; font-size: 14px; }}
    .panel {{ margin-top: 22px; background: var(--paper); border: 1px solid var(--line); border-radius: 22px; padding: 22px; box-shadow: var(--shadow); }}
    table {{ width: 100%; border-collapse: collapse; font-size: 14px; }}
    th, td {{ text-align: left; padding: 10px 8px; border-bottom: 1px solid var(--line); vertical-align: top; }}
    th {{ color: var(--muted); font-weight: 600; font-size: 12px; text-transform: uppercase; letter-spacing: 0.06em; }}
    .inline-link {{ color: var(--accent); text-decoration: none; font-weight: 600; }}
    .inline-link:hover {{ text-decoration: underline; }}
    .badge {{
      display: inline-flex; align-items: center; justify-content: center; padding: 6px 10px; border-radius: 999px;
      font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: #fff;
    }}
    .badge-critical {{ background: var(--critical); }}
    .badge-high {{ background: var(--high); }}
    .badge-medium {{ background: var(--medium); }}
    .badge-low {{ background: var(--low); }}
    .badge-none {{ background: var(--none); }}
  </style>
</head>
<body>
  <main class="shell">
    <section class="hero">
      <h1>Проблемные компании за {days} {('день' if days == 1 else 'дней')}</h1>
      <p>Список собран по live company signals. В приоритете max score, плотность сигналов и общий вес проблемного контура.</p>
      <div class="hero-links">
        <a href="/manager/brief">Текущий brief</a>
        <a href="/manager/briefs">История brief</a>
        <a href="/manager/trends/weekly">Неделя</a>
        <a href="/manager/recovery">AI recovery</a>
        <a href="/manager/actions">Очередь действий</a>
        <a href="/manager/problematic?days=1">Срез 1д</a>
        <a href="/manager/problematic?days=7">Срез 7д</a>
      </div>
    </section>
    <section class="panel">
      <table>
        <thead>
          <tr>
            <th>Компания</th>
            <th>Нормализация</th>
            <th>Severity</th>
            <th>Max score</th>
            <th>Signals</th>
            <th>Critical</th>
            <th>Активность 30д</th>
            <th>Прогноз 30д</th>
            <th>Top signal</th>
            <th>Комментарий</th>
          </tr>
        </thead>
        <tbody>
          {''.join(rows) or '<tr><td colspan="10">Нет сигналов за выбранный период.</td></tr>'}
        </tbody>
      </table>
    </section>
  </main>
</body>
</html>"""


def render_management_actions_html(
    items: list[dict[str, Any]],
    companies: list[dict[str, Any]],
    priority_tier: str | None = None,
    owner: str | None = None,
) -> str:
    rows = []
    counts = {"critical": 0, "high": 0, "medium": 0, "low": 0}
    owners: dict[str, int] = {}
    for item in items:
        tier = str(item.get("priority_tier") or "low").lower()
        if tier in counts:
            counts[tier] += 1
        owner_name = str(item.get("owner_name") or "не назначен")
        owners[owner_name] = owners.get(owner_name, 0) + 1
        rows.append(
            "<tr>"
            f"<td>{severity_badge(str(item.get('priority_tier') or 'low'))}</td>"
            f"<td>{html.escape(str(item.get('deadline_hint') or '-'))}</td>"
            f"<td><a class=\"inline-link\" href=\"{company_detail_url(str(item.get('company_entity_key') or item.get('counterparty') or '-'), str(item.get('infobase') or '') or None)}\">{html.escape(str(item.get('counterparty') or '-'))}</a></td>"
            f"<td>{html.escape(owner_name)}</td>"
            f"<td>{html.escape(str(item.get('action_type') or '-'))}</td>"
            f"<td>{html.escape(fmt_number(item.get('priority_score')))}</td>"
            f"<td>{html.escape(str(item.get('recommended_action') or '-'))}</td>"
            f"<td>{html.escape(str(item.get('reason') or '-'))}</td>"
            f"<td>{html.escape(str(item.get('evidence_summary') or '-'))}</td>"
            "</tr>"
        )

    company_rows = []
    companies_critical = 0
    companies_unassigned = 0
    for item in companies:
        tier = str(item.get("priority_tier") or "low").lower()
        if tier == "critical":
            companies_critical += 1
        owner_names = str(item.get("owner_names") or "не назначен")
        if owner_names == "не назначен":
            companies_unassigned += 1
        company_rows.append(
            "<tr>"
            f"<td>{severity_badge(tier)}</td>"
            f"<td><a class=\"inline-link\" href=\"{company_detail_url(str(item.get('company_entity_key') or item.get('counterparty') or '-'), str(item.get('infobase') or '') or None)}\">{html.escape(str(item.get('counterparty') or '-'))}</a></td>"
            f"<td>{html.escape(fmt_number(item.get('actions_total')))}</td>"
            f"<td>{html.escape(str(item.get('action_types_summary') or '-'))}</td>"
            f"<td>{html.escape(str(item.get('recommended_action_summary') or '-'))}</td>"
            f"<td>{html.escape(str(item.get('reason_summary') or '-'))}</td>"
            f"<td>{html.escape(owner_names)}</td>"
            "</tr>"
        )

    top_owners = sorted(owners.items(), key=lambda pair: (-pair[1], pair[0]))[:5]
    owner_items = "".join(
        f"<li>{html.escape(name)}: {html.escape(fmt_number(total))} действий</li>"
        for name, total in top_owners
    )

    filters: list[str] = []
    if priority_tier:
        filters.append(f"priority_tier={priority_tier}")
    if owner:
        filters.append(f"owner={owner}")
    filter_line = ", ".join(filters) if filters else "без фильтра"
    query_suffix = _actions_query_suffix(priority_tier=priority_tier, owner=owner)
    raw_json_url = f"/api/1/analytics-1c/manager/actions{query_suffix}"
    companies_json_url = f"/api/1/analytics-1c/manager/actions/companies{query_suffix}"

    return f"""<!doctype html>
<html lang="ru">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta http-equiv="refresh" content="300">
  <title>1C Manager Actions</title>
  <style>
    :root {{
      --bg: #f4f1ea; --paper: #fffdf8; --ink: #1c1a17; --muted: #6b655c; --line: #d8d0c4; --accent: #005f73; --shadow: 0 14px 40px rgba(28, 26, 23, 0.08);
      --critical: #9b2226; --high: #bb3e03; --medium: #ca6702; --low: #4d7c0f; --none: #687076;
    }}
    * {{ box-sizing: border-box; }}
    body {{ margin: 0; font-family: "IBM Plex Sans", "Segoe UI", system-ui, sans-serif; background: linear-gradient(180deg, #faf7f2 0%, var(--bg) 100%); color: var(--ink); }}
    .shell {{ max-width: 1560px; margin: 0 auto; padding: 28px 22px 60px; }}
    .hero {{ background: linear-gradient(135deg, rgba(0,95,115,0.94), rgba(10,77,104,0.92)); color: #f8fbfc; border-radius: 24px; padding: 28px 30px; box-shadow: var(--shadow); }}
    .hero h1 {{ margin: 0 0 10px; font-size: clamp(28px, 4vw, 42px); }}
    .hero p {{ margin: 0 0 14px; line-height: 1.5; color: rgba(248,251,252,0.88); }}
    .hero-links {{ display: flex; gap: 10px; flex-wrap: wrap; }}
    .hero-links a {{ text-decoration: none; color: #f8fbfc; border: 1px solid rgba(248, 251, 252, 0.28); padding: 9px 12px; border-radius: 999px; font-size: 14px; }}
    .grid {{ display: grid; grid-template-columns: repeat(12, minmax(0, 1fr)); gap: 18px; margin-top: 22px; }}
    .panel {{ background: var(--paper); border: 1px solid var(--line); border-radius: 22px; padding: 22px; box-shadow: var(--shadow); }}
    .span-12 {{ grid-column: span 12; }} .span-8 {{ grid-column: span 8; }} .span-4 {{ grid-column: span 4; }}
    .stats {{ display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; }}
    .stat {{ padding: 16px; border-radius: 18px; background: linear-gradient(180deg, #fff 0%, #f7f4ee 100%); border: 1px solid var(--line); }}
    .stat-label {{ color: var(--muted); font-size: 13px; margin-bottom: 8px; }}
    .stat-value {{ font-size: 24px; font-weight: 700; letter-spacing: -0.03em; }}
    table {{ width: 100%; border-collapse: collapse; font-size: 14px; }}
    th, td {{ text-align: left; padding: 10px 8px; border-bottom: 1px solid var(--line); vertical-align: top; }}
    th {{ color: var(--muted); font-weight: 600; font-size: 12px; text-transform: uppercase; letter-spacing: 0.06em; }}
    .inline-link {{ color: var(--accent); text-decoration: none; font-weight: 600; }}
    .inline-link:hover {{ text-decoration: underline; }}
    .badge {{
      display: inline-flex; align-items: center; justify-content: center; padding: 6px 10px; border-radius: 999px;
      font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: #fff;
    }}
    .badge-critical {{ background: var(--critical); }}
    .badge-high {{ background: var(--high); }}
    .badge-medium {{ background: var(--medium); }}
    .badge-low {{ background: var(--low); }}
    .badge-none {{ background: var(--none); }}
    ul {{ margin: 0; padding-left: 20px; line-height: 1.55; }}
    @media (max-width: 1100px) {{ .span-8, .span-4 {{ grid-column: span 12; }} .stats {{ grid-template-columns: repeat(2, minmax(0, 1fr)); }} }}
    @media (max-width: 640px) {{ .shell {{ padding: 16px 14px 40px; }} .stats {{ grid-template-columns: 1fr; }} th:nth-child(8), td:nth-child(8), th:nth-child(9), td:nth-child(9) {{ display: none; }} }}
  </style>
</head>
<body>
  <main class="shell">
    <section class="hero">
      <h1>Очередь управленческих действий по 1С</h1>
      <p>Детерминированный слой поверх portfolio/signals/business events/document changes. Фильтр: {html.escape(filter_line)}.</p>
      <p>Приоритет считается по риску предприятия. Текущее назначение используется только как operational metadata и не меняет порядок очереди.</p>
      <div class="hero-links">
        <a href="/manager/brief">Текущий brief</a>
        <a href="/manager/changes">Что изменилось</a>
        <a href="/manager/trends/weekly">Неделя</a>
        <a href="/manager/recovery">AI recovery</a>
        <a href="/manager/problematic?days=7">Проблемные 7д</a>
        <a href="{html.escape(companies_json_url)}">JSON по предприятиям</a>
        <a href="{html.escape(raw_json_url)}">Raw actions JSON</a>
      </div>
    </section>
    <section class="grid">
      <article class="panel span-8">
        <h2>Срез очереди</h2>
        <div class="stats">
          <div class="stat"><div class="stat-label">Всего действий</div><div class="stat-value">{html.escape(fmt_number(len(items)))}</div></div>
          <div class="stat"><div class="stat-label">Предприятий</div><div class="stat-value">{html.escape(fmt_number(len(companies)))}</div></div>
          <div class="stat"><div class="stat-label">Critical предприятий</div><div class="stat-value">{html.escape(fmt_number(companies_critical))}</div></div>
          <div class="stat"><div class="stat-label">Без владельца</div><div class="stat-value">{html.escape(fmt_number(companies_unassigned))}</div></div>
        </div>
      </article>
      <article class="panel span-4">
        <h2>Текущее назначение</h2>
        <ul>{owner_items or '<li>Нет назначенных действий.</li>'}</ul>
      </article>
      <article class="panel span-12">
        <h2>Что делать по предприятиям</h2>
        <table>
          <thead>
            <tr>
              <th>Приоритет</th>
              <th>Предприятие</th>
              <th>Действий</th>
              <th>Контур</th>
              <th>Что сделать</th>
              <th>Почему</th>
              <th>Текущее назначение</th>
            </tr>
          </thead>
          <tbody>
            {''.join(company_rows) or '<tr><td colspan="7">Нет компаний с активными действиями.</td></tr>'}
          </tbody>
        </table>
      </article>
      <article class="panel span-12">
        <h2>Детализация действий</h2>
        <table>
          <thead>
            <tr>
              <th>Приоритет</th>
              <th>Срок</th>
              <th>Компания</th>
              <th>Ответственный</th>
              <th>Тип</th>
              <th>Score</th>
              <th>Действие</th>
              <th>Почему</th>
              <th>Evidence</th>
            </tr>
          </thead>
          <tbody>
            {''.join(rows) or '<tr><td colspan="9">Действий не найдено.</td></tr>'}
          </tbody>
        </table>
      </article>
    </section>
  </main>
</body>
</html>"""


def render_brief_delta_html(payload: dict[str, Any]) -> str:
    delta = extract_delta(payload)
    brief = payload.get("brief", {})
    if not delta.get("available"):
        return f"""<!doctype html>
<html lang="ru"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>1C Brief Changes</title></head><body><main style="max-width:980px;margin:40px auto;font-family:IBM Plex Sans,Segoe UI,sans-serif">
<h1>Что изменилось с прошлого запуска</h1><p>Delta пока недоступна: {html.escape(str(delta.get('reason', 'unknown')))}.</p>
<p><a href="/manager/brief">Вернуться к brief</a></p></main></body></html>"""

    summary = delta.get("summary", {})
    top_changes = delta.get("top_changes", [])
    new_critical = delta.get("new_critical", [])
    resolved_critical = delta.get("resolved_critical", [])
    entered_watchlist = delta.get("entered_watchlist", [])
    left_watchlist = delta.get("left_watchlist", [])

    def delta_value(value: Any) -> str:
        try:
            numeric = float(value)
        except (TypeError, ValueError):
            return html.escape(str(value))
        prefix = "+" if numeric > 0 else ""
        if numeric.is_integer():
            return prefix + str(int(numeric))
        return prefix + f"{numeric:.2f}".replace(".", ",")

    stat_cards = [
        ("Critical", delta_value(summary.get("critical_total_delta", 0))),
        ("Busy", delta_value(summary.get("busy_total_delta", 0))),
        ("Кейсы", delta_value(summary.get("open_cases_total_delta", 0))),
        ("Detections", delta_value(summary.get("detections_total_delta", 0))),
        ("Активность 30д", delta_value(summary.get("activity_30d_total_delta", 0))),
        ("Прогноз 30д", delta_value(summary.get("activity_forecast_30d_total_delta", 0))),
    ]
    stat_html = "".join(
        f"<div class=\"stat\"><div class=\"stat-label\">{html.escape(label)}</div><div class=\"stat-value\">{html.escape(value)}</div></div>"
        for label, value in stat_cards
    )
    tier_labels = {
        "critical": "Критический",
        "high": "Высокий",
        "medium": "Средний",
        "low": "Низкий",
    }

    rows = []
    for item in top_changes:
        infobase = item.get("infobase")
        counterparty = item.get("company")
        priority_tier = str(item.get("priority_tier") or "low")
        rows.append(
            "<tr>"
            f"<td><a class=\"inline-link\" href=\"{company_detail_url(str(item.get('company_entity_key') or counterparty), str(infobase) if infobase else None)}\">{html.escape(str(counterparty or '-'))}</a></td>"
            f"<td>{html.escape(tier_labels.get(priority_tier, priority_tier))}</td>"
            f"<td>{delta_value(item.get('priority_score', 0))}</td>"
            f"<td>{html.escape(str(item.get('change_type') or '-'))}</td>"
            f"<td>{html.escape(str(item.get('severity_before') or '-'))} -> {html.escape(str(item.get('severity_after') or '-'))}</td>"
            f"<td>{delta_value(item.get('score_delta', 0))}</td>"
            f"<td>{delta_value(item.get('open_cases_delta', 0))}</td>"
            f"<td>{delta_value(item.get('active_locks_delta', 0))}</td>"
            f"<td>{delta_value(item.get('forecast_delta', 0))}</td>"
            f"<td>{html.escape(str(item.get('priority_reason') or '-'))}</td>"
            f"<td>{html.escape(str(item.get('summary') or '-'))}</td>"
            "</tr>"
        )

    def as_list(items: list[str]) -> str:
        if not items:
            return "<li>Нет</li>"
        return "".join(f"<li>{html.escape(str(item))}</li>" for item in items)

    return f"""<!doctype html>
<html lang="ru">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta http-equiv="refresh" content="300">
  <title>1C Brief Changes</title>
  <style>
    :root {{ --bg:#f4f1ea; --paper:#fffdf8; --ink:#1c1a17; --muted:#6b655c; --line:#d8d0c4; --accent:#005f73; --shadow:0 14px 40px rgba(28,26,23,.08); }}
    * {{ box-sizing:border-box; }}
    body {{ margin:0; font-family:"IBM Plex Sans","Segoe UI",system-ui,sans-serif; color:var(--ink); background:linear-gradient(180deg,#faf7f2 0%,var(--bg) 100%); }}
    .shell {{ max-width:1480px; margin:0 auto; padding:28px 22px 60px; }}
    .hero {{ background:linear-gradient(135deg,rgba(0,95,115,.94),rgba(10,77,104,.92)); color:#f8fbfc; border-radius:24px; padding:28px 30px; box-shadow:var(--shadow); }}
    .hero h1 {{ margin:0 0 10px; font-size:clamp(28px,4vw,42px); }}
    .hero p {{ margin:0 0 14px; color:rgba(248,251,252,.88); line-height:1.5; }}
    .hero-links {{ display:flex; gap:10px; flex-wrap:wrap; }}
    .hero-links a {{ text-decoration:none; color:#f8fbfc; border:1px solid rgba(248,251,252,.28); padding:9px 12px; border-radius:999px; font-size:14px; }}
    .grid {{ display:grid; grid-template-columns:repeat(12,minmax(0,1fr)); gap:18px; margin-top:22px; }}
    .panel {{ background:var(--paper); border:1px solid var(--line); border-radius:22px; padding:22px; box-shadow:var(--shadow); }}
    .span-12 {{ grid-column:span 12; }} .span-6 {{ grid-column:span 6; }} .span-4 {{ grid-column:span 4; }}
    .stats {{ display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:12px; }}
    .stat {{ padding:16px; border-radius:18px; background:linear-gradient(180deg,#fff 0%,#f7f4ee 100%); border:1px solid var(--line); }}
    .stat-label {{ color:var(--muted); font-size:13px; margin-bottom:8px; }}
    .stat-value {{ font-size:24px; font-weight:700; letter-spacing:-.03em; }}
    table {{ width:100%; border-collapse:collapse; font-size:14px; }}
    th,td {{ text-align:left; padding:10px 8px; border-bottom:1px solid var(--line); vertical-align:top; }}
    th {{ color:var(--muted); font-weight:600; font-size:12px; text-transform:uppercase; letter-spacing:.06em; }}
    .inline-link {{ color:var(--accent); text-decoration:none; font-weight:600; }}
    .inline-link:hover {{ text-decoration:underline; }}
    ul {{ margin:0; padding-left:20px; line-height:1.55; }}
    @media (max-width:1100px) {{ .span-6,.span-4 {{ grid-column:span 12; }} .stats {{ grid-template-columns:repeat(2,minmax(0,1fr)); }} }}
    @media (max-width:640px) {{ .shell {{ padding:16px 14px 40px; }} .hero {{ padding:22px 18px; }} .stats {{ grid-template-columns:1fr; }} }}
  </style>
</head>
<body>
  <main class="shell">
    <section class="hero">
      <h1>Что изменилось с прошлого запуска</h1>
      <p>Сравнение запусков: {html.escape(str(delta.get('previous_generated_at') or '-'))} -> {html.escape(str(delta.get('current_generated_at') or payload.get('generated_at') or '-'))}.</p>
      <div class="hero-links">
        <a href="/manager/brief">Текущий brief</a>
        <a href="/manager/briefs">История brief</a>
        <a href="/manager/trends/weekly">Неделя</a>
        <a href="/manager/digest/weekly">Weekly digest</a>
        <a href="/manager/recovery">AI recovery</a>
        <a href="/manager/problematic?days=1">Проблемные 1д</a>
        <a href="/manager/problematic?days=7">Проблемные 7д</a>
      </div>
    </section>

    <section class="grid">
      <article class="panel span-12">
        <h2>Сводка изменений</h2>
        <div class="stats">{stat_html}</div>
      </article>

      <article class="panel span-6">
        <h2>Новые critical</h2>
        <ul>{as_list(new_critical)}</ul>
      </article>

      <article class="panel span-6">
        <h2>Вышли из critical</h2>
        <ul>{as_list(resolved_critical)}</ul>
      </article>

      <article class="panel span-6">
        <h2>Зашли в watchlist</h2>
        <ul>{as_list(entered_watchlist)}</ul>
      </article>

      <article class="panel span-6">
        <h2>Вышли из watchlist</h2>
        <ul>{as_list(left_watchlist)}</ul>
      </article>

      <article class="panel span-12">
        <h2>Ключевые изменения</h2>
        <table>
          <thead>
            <tr>
              <th>Компания</th>
              <th>Приоритет</th>
              <th>Score</th>
              <th>Тип</th>
              <th>Severity</th>
              <th>Score Δ</th>
              <th>Cases Δ</th>
              <th>Locks Δ</th>
              <th>Forecast Δ</th>
              <th>Причина приоритета</th>
              <th>Комментарий</th>
            </tr>
          </thead>
          <tbody>
            {''.join(rows) or '<tr><td colspan="11">Нет выраженных изменений.</td></tr>'}
          </tbody>
        </table>
      </article>
    </section>
  </main>
</body>
</html>"""


def render_weekly_trend_html(report: dict[str, Any]) -> str:
    daily = report.get("daily", [])
    latest = report.get("latest") or {}
    top_weekly_changes = report.get("top_weekly_changes", [])

    def value(v: Any) -> str:
        return fmt_number(v)

    trend_rows = []
    for item in daily:
        trend_rows.append(
            "<tr>"
            f"<td>{html.escape(str(item.get('date', '-')))}</td>"
            f"<td>{value(item.get('companies_total'))}</td>"
            f"<td>{value(item.get('critical_total'))}</td>"
            f"<td>{value(item.get('busy_total'))}</td>"
            f"<td>{value(item.get('open_cases_total'))}</td>"
            f"<td>{value(item.get('detections_total'))}</td>"
            f"<td>{value(item.get('activity_30d_total'))}</td>"
            f"<td>{value(item.get('activity_forecast_30d_total'))}</td>"
            f"<td>{value(item.get('critical_delta_vs_prev_day'))}</td>"
            f"<td>{value(item.get('open_cases_delta_vs_prev_day'))}</td>"
            "</tr>"
        )

    change_rows = []
    for item in top_weekly_changes:
        infobase = item.get("infobase")
        company = item.get("company")
        change_rows.append(
            "<tr>"
            f"<td><a class=\"inline-link\" href=\"{company_detail_url(str(company), str(infobase) if infobase else None)}\">{html.escape(str(company or '-'))}</a></td>"
            f"<td>{html.escape(str(item.get('priority_tier') or '-'))}</td>"
            f"<td>{value(item.get('priority_score'))}</td>"
            f"<td>{html.escape(str(item.get('change_type') or '-'))}</td>"
            f"<td>{value(item.get('open_cases_delta'))}</td>"
            f"<td>{value(item.get('active_locks_delta'))}</td>"
            f"<td>{value(item.get('forecast_delta'))}</td>"
            f"<td>{html.escape(str(item.get('priority_reason') or '-'))}</td>"
            "</tr>"
        )

    stat_cards = [
        ("Компаний", value(latest.get("companies_total"))),
        ("Critical", value(latest.get("critical_total"))),
        ("Busy", value(latest.get("busy_total"))),
        ("Кейсы", value(latest.get("open_cases_total"))),
        ("Detections", value(latest.get("detections_total"))),
        ("Прогноз 30д", value(latest.get("activity_forecast_30d_total"))),
    ]
    stat_html = "".join(
        f"<div class=\"stat\"><div class=\"stat-label\">{html.escape(label)}</div><div class=\"stat-value\">{html.escape(val)}</div></div>"
        for label, val in stat_cards
    )

    return f"""<!doctype html>
<html lang="ru">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta http-equiv="refresh" content="300">
  <title>1C Weekly Trends</title>
  <style>
    :root {{ --bg:#f4f1ea; --paper:#fffdf8; --ink:#1c1a17; --muted:#6b655c; --line:#d8d0c4; --accent:#005f73; --shadow:0 14px 40px rgba(28,26,23,.08); }}
    * {{ box-sizing:border-box; }}
    body {{ margin:0; font-family:"IBM Plex Sans","Segoe UI",system-ui,sans-serif; color:var(--ink); background:linear-gradient(180deg,#faf7f2 0%,var(--bg) 100%); }}
    .shell {{ max-width:1480px; margin:0 auto; padding:28px 22px 60px; }}
    .hero {{ background:linear-gradient(135deg,rgba(0,95,115,.94),rgba(10,77,104,.92)); color:#f8fbfc; border-radius:24px; padding:28px 30px; box-shadow:var(--shadow); }}
    .hero h1 {{ margin:0 0 10px; font-size:clamp(28px,4vw,42px); }}
    .hero p {{ margin:0 0 14px; color:rgba(248,251,252,.88); line-height:1.5; }}
    .hero-links {{ display:flex; gap:10px; flex-wrap:wrap; }}
    .hero-links a {{ text-decoration:none; color:#f8fbfc; border:1px solid rgba(248,251,252,.28); padding:9px 12px; border-radius:999px; font-size:14px; }}
    .panel {{ margin-top:22px; background:var(--paper); border:1px solid var(--line); border-radius:22px; padding:22px; box-shadow:var(--shadow); }}
    .stats {{ display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:12px; }}
    .stat {{ padding:16px; border-radius:18px; background:linear-gradient(180deg,#fff 0%,#f7f4ee 100%); border:1px solid var(--line); }}
    .stat-label {{ color:var(--muted); font-size:13px; margin-bottom:8px; }}
    .stat-value {{ font-size:24px; font-weight:700; letter-spacing:-.03em; }}
    table {{ width:100%; border-collapse:collapse; font-size:14px; }}
    th,td {{ text-align:left; padding:10px 8px; border-bottom:1px solid var(--line); vertical-align:top; }}
    th {{ color:var(--muted); font-weight:600; font-size:12px; text-transform:uppercase; letter-spacing:.06em; }}
    .inline-link {{ color:var(--accent); text-decoration:none; font-weight:600; }}
    .inline-link:hover {{ text-decoration:underline; }}
    @media (max-width:1100px) {{ .stats {{ grid-template-columns:repeat(2,minmax(0,1fr)); }} }}
    @media (max-width:640px) {{ .shell {{ padding:16px 14px 40px; }} .hero {{ padding:22px 18px; }} .stats {{ grid-template-columns:1fr; }} }}
  </style>
</head>
<body>
  <main class="shell">
    <section class="hero">
      <h1>Недельный тренд портфеля</h1>
      <p>Период: {html.escape(str(report.get('period_start', '-')))} -> {html.escape(str(report.get('period_end', '-')))}. Страница показывает тренд по истории executive brief и недельный рейтинг проблемных компаний.</p>
      <div class="hero-links">
        <a href="/manager/brief">Текущий brief</a>
        <a href="/manager/changes">Что изменилось</a>
        <a href="/manager/briefs">История brief</a>
        <a href="/manager/problematic?days=7">Проблемные 7д</a>
        <a href="/manager/recovery">AI recovery</a>
        <a href="/manager/digest/weekly">Weekly digest</a>
      </div>
    </section>
    <section class="panel">
      <h2>Текущее состояние по последнему дневному срезу</h2>
      <div class="stats">{stat_html}</div>
    </section>
    <section class="panel">
      <h2>Дневной тренд</h2>
      <table>
        <thead>
          <tr>
            <th>Дата</th>
            <th>Компаний</th>
            <th>Critical</th>
            <th>Busy</th>
            <th>Кейсы</th>
            <th>Detections</th>
            <th>Активность 30д</th>
            <th>Прогноз 30д</th>
            <th>Critical Δ</th>
            <th>Cases Δ</th>
          </tr>
        </thead>
        <tbody>
          {''.join(trend_rows) or '<tr><td colspan="10">Недостаточно истории для недельного тренда.</td></tr>'}
        </tbody>
      </table>
    </section>
    <section class="panel">
      <h2>Недельный рейтинг приоритетов</h2>
      <table>
        <thead>
          <tr>
            <th>Компания</th>
            <th>Приоритет</th>
            <th>Priority score</th>
            <th>Тип</th>
            <th>Cases Δ</th>
            <th>Locks Δ</th>
            <th>Forecast Δ</th>
            <th>Причина</th>
          </tr>
        </thead>
        <tbody>
          {''.join(change_rows) or '<tr><td colspan="8">За выбранный период заметных изменений нет.</td></tr>'}
        </tbody>
      </table>
    </section>
  </main>
</body>
</html>"""


def render_weekly_digest_html(payload: dict[str, Any]) -> str:
    digest = payload.get("digest", {})
    context = payload.get("context", {})
    latest = context.get("latest_summary", {})
    top_priorities = digest.get("top_priorities", [])
    improvements = digest.get("improvements", [])
    actions = digest.get("actions", [])
    caveats = digest.get("caveats", [])
    summary = digest.get("summary", [])
    generated_at = payload.get("generated_at", "")

    stat_cards = [
        ("Компаний", fmt_number(latest.get("companies_total"))),
        ("Critical", fmt_number(latest.get("critical_total"))),
        ("Busy", fmt_number(latest.get("busy_total"))),
        ("Кейсы", fmt_number(latest.get("open_cases_total"))),
        ("Detections", fmt_number(latest.get("detections_total"))),
        ("Прогноз 30д", fmt_number(latest.get("activity_forecast_30d_total"))),
    ]
    stat_html = "".join(
        f"<div class=\"stat\"><div class=\"stat-label\">{html.escape(label)}</div><div class=\"stat-value\">{html.escape(value)}</div></div>"
        for label, value in stat_cards
    )

    priority_cards = []
    for item in top_priorities:
        company = str(item.get("company") or "-")
        priority_cards.append(
            "<article class=\"stack-card\">"
            f"<div class=\"stack-card-head\"><h3>{html.escape(company)}</h3>{severity_badge(str(item.get('priority') or 'low'))}</div>"
            f"<p class=\"stack-card-body\">{html.escape(str(item.get('reason') or '-'))}</p>"
            f"<p class=\"stack-card-action\"><strong>Действие:</strong> {html.escape(str(item.get('recommended_action') or '-'))}</p>"
            f"<p class=\"stack-card-action\"><a class=\"inline-link\" href=\"{company_detail_url(str(item.get('company_entity_key') or company), str(item.get('infobase') or '') or None)}\">Карточка компании</a></p>"
            "</article>"
        )

    improvement_rows = []
    for item in improvements:
        improvement_rows.append(
            "<tr>"
            f"<td><a class=\"inline-link\" href=\"{company_detail_url(str(item.get('company') or '-'), str(item.get('infobase') or '') or None)}\">{html.escape(str(item.get('company') or '-'))}</a></td>"
            f"<td>{html.escape(str(item.get('signal') or '-'))}</td>"
            f"<td>{html.escape(str(item.get('meaning') or '-'))}</td>"
            "</tr>"
        )

    summary_items = "\n".join(f"<li>{html.escape(str(item))}</li>" for item in summary)
    action_items = "\n".join(f"<li>{html.escape(str(item))}</li>" for item in actions)
    caveat_items = "\n".join(f"<li>{html.escape(str(item))}</li>" for item in caveats)

    return f"""<!doctype html>
<html lang="ru">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta http-equiv="refresh" content="300">
  <title>1C Weekly Digest</title>
  <style>
    :root {{
      --bg:#f4f1ea; --paper:#fffdf8; --ink:#1c1a17; --muted:#6b655c; --line:#d8d0c4; --accent:#005f73;
      --critical:#9b2226; --high:#bb3e03; --medium:#ca6702; --low:#4d7c0f; --none:#687076; --shadow:0 14px 40px rgba(28,26,23,.08);
    }}
    * {{ box-sizing:border-box; }}
    body {{ margin:0; font-family:"IBM Plex Sans","Segoe UI",system-ui,sans-serif; color:var(--ink); background:linear-gradient(180deg,#faf7f2 0%,var(--bg) 100%); }}
    .shell {{ max-width:1480px; margin:0 auto; padding:28px 22px 60px; }}
    .hero {{ background:linear-gradient(135deg,rgba(0,95,115,.94),rgba(10,77,104,.92)); color:#f8fbfc; border-radius:24px; padding:28px 30px; box-shadow:var(--shadow); }}
    .hero h1 {{ margin:0 0 10px; font-size:clamp(28px,4vw,42px); }}
    .hero p {{ margin:0 0 14px; color:rgba(248,251,252,.88); line-height:1.5; }}
    .hero-links {{ display:flex; gap:10px; flex-wrap:wrap; }}
    .hero-links a {{ text-decoration:none; color:#f8fbfc; border:1px solid rgba(248,251,252,.28); padding:9px 12px; border-radius:999px; font-size:14px; }}
    .grid {{ display:grid; grid-template-columns:repeat(12,minmax(0,1fr)); gap:18px; margin-top:22px; }}
    .panel {{ background:var(--paper); border:1px solid var(--line); border-radius:22px; padding:22px; box-shadow:var(--shadow); }}
    .span-12 {{ grid-column:span 12; }} .span-8 {{ grid-column:span 8; }} .span-6 {{ grid-column:span 6; }} .span-4 {{ grid-column:span 4; }}
    .stats {{ display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:12px; }}
    .stat {{ padding:16px; border-radius:18px; background:linear-gradient(180deg,#fff 0%,#f7f4ee 100%); border:1px solid var(--line); }}
    .stat-label {{ color:var(--muted); font-size:13px; margin-bottom:8px; }}
    .stat-value {{ font-size:24px; font-weight:700; letter-spacing:-.03em; }}
    .stack {{ display:grid; gap:14px; }}
    .stack-card {{ border:1px solid var(--line); border-radius:18px; padding:16px 18px; background:#fffdfa; }}
    .stack-card-head {{ display:flex; justify-content:space-between; align-items:center; gap:12px; margin-bottom:10px; }}
    .stack-card-body,.stack-card-action {{ margin:0; line-height:1.5; }}
    .stack-card-action {{ margin-top:10px; color:var(--muted); }}
    .badge {{ display:inline-flex; align-items:center; justify-content:center; padding:6px 10px; border-radius:999px; font-size:12px; font-weight:700; text-transform:uppercase; letter-spacing:.06em; color:#fff; }}
    .badge-critical {{ background:var(--critical); }} .badge-high {{ background:var(--high); }} .badge-medium {{ background:var(--medium); }} .badge-low {{ background:var(--low); }} .badge-none {{ background:var(--none); }}
    .inline-link {{ color:var(--accent); text-decoration:none; font-weight:600; }} .inline-link:hover {{ text-decoration:underline; }}
    table {{ width:100%; border-collapse:collapse; font-size:14px; }}
    th,td {{ text-align:left; padding:10px 8px; border-bottom:1px solid var(--line); vertical-align:top; }}
    th {{ color:var(--muted); font-weight:600; font-size:12px; text-transform:uppercase; letter-spacing:.06em; }}
    ul {{ margin:0; padding-left:20px; line-height:1.55; }}
    @media (max-width:1100px) {{ .span-8,.span-6,.span-4 {{ grid-column:span 12; }} .stats {{ grid-template-columns:repeat(2,minmax(0,1fr)); }} }}
    @media (max-width:640px) {{ .shell {{ padding:16px 14px 40px; }} .hero {{ padding:22px 18px; }} .stats {{ grid-template-columns:1fr; }} }}
  </style>
</head>
<body>
  <main class="shell">
    <section class="hero">
      <h1>{html.escape(str(digest.get('headline') or 'Weekly digest'))}</h1>
      <p>Период: {html.escape(str(context.get('period_start') or '-'))} -> {html.escape(str(context.get('period_end') or '-'))}. Сформировано: {html.escape(str(generated_at or '-'))}.</p>
      <div class="hero-links">
        <a href="/manager/brief">Текущий brief</a>
        <a href="/manager/changes">Что изменилось</a>
        <a href="/manager/trends/weekly">Неделя</a>
        <a href="/manager/problematic?days=7">Проблемные 7д</a>
        <a href="/manager/recovery">AI recovery</a>
        <a href="/api/1/analytics-1c/manager/digest/weekly/latest">JSON</a>
        <a href="/api/1/analytics-1c/manager/digest/weekly/latest.md">Markdown</a>
      </div>
    </section>

    <section class="grid">
      <article class="panel span-8">
        <h2>Кратко для руководителя</h2>
        <ul>{summary_items}</ul>
      </article>
      <article class="panel span-4">
        <h2>Срез недели</h2>
        <div class="stats">{stat_html}</div>
      </article>

      <article class="panel span-8">
        <h2>Компании первой очереди</h2>
        <div class="stack">{''.join(priority_cards) or '<p>Нет weekly priorities.</p>'}</div>
      </article>

      <article class="panel span-4">
        <h2>Что делать</h2>
        <ul>{action_items}</ul>
      </article>

      <article class="panel span-12">
        <h2>Что улучшилось за неделю</h2>
        <table>
          <thead>
            <tr><th>Компания</th><th>Сигнал</th><th>Что это значит</th></tr>
          </thead>
          <tbody>
            {''.join(improvement_rows) or '<tr><td colspan="3">Явных улучшений за неделю не зафиксировано.</td></tr>'}
          </tbody>
        </table>
      </article>

      <article class="panel span-12">
        <h2>Ограничения интерпретации</h2>
        <ul>{caveat_items}</ul>
      </article>
    </section>
  </main>
</body>
</html>"""


def render_recovery_brief_html(payload: dict[str, Any]) -> str:
    recovery = payload.get("recovery", {})
    generated_at = payload.get("generated_at", "")
    render_mode = payload.get("render_mode", "unknown")
    situation_items = "".join(f"<li>{html.escape(str(item))}</li>" for item in recovery.get("situation", []))
    action_items = "".join(f"<li>{html.escape(str(item))}</li>" for item in recovery.get("portfolio_actions", []))
    caveat_items = "".join(f"<li>{html.escape(str(item))}</li>" for item in recovery.get("caveats", []))
    incident_cards = []
    for item in recovery.get("top_incidents", []):
        company = str(item.get("company") or "-")
        actions = "".join(f"<li>{html.escape(str(action))}</li>" for action in item.get("actions", []))
        incident_cards.append(
            "<article class=\"stack-card\">"
            f"<div class=\"stack-card-head\"><h3>{html.escape(company)}</h3>{severity_badge(str(item.get('severity') or 'critical'))}</div>"
            f"<p class=\"stack-card-body\"><strong>Диагноз:</strong> {html.escape(str(item.get('diagnosis') or '-'))}</p>"
            f"<p class=\"stack-card-body\"><strong>Что не делать:</strong> {html.escape(str(item.get('stop_doing') or '-'))}</p>"
            f"<p class=\"stack-card-body\"><strong>Цель 24ч:</strong> {html.escape(str(item.get('target_state_24h') or '-'))}</p>"
            f"<ul class=\"meta-list\">{actions or '<li>Нет действий.</li>'}</ul>"
            f"<p class=\"stack-card-action\"><a class=\"inline-link\" href=\"{company_detail_url(company)}\">Карточка компании</a></p>"
            "</article>"
        )
    return f"""<!doctype html>
<html lang="ru">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta http-equiv="refresh" content="300">
  <title>1C Recovery Brief</title>
  <style>
    :root {{
      --bg:#f4f1ea; --paper:#fffdf8; --ink:#1c1a17; --muted:#6b655c; --line:#d8d0c4; --accent:#005f73;
      --critical:#9b2226; --high:#bb3e03; --medium:#ca6702; --low:#4d7c0f; --none:#687076; --shadow:0 14px 40px rgba(28,26,23,.08);
    }}
    * {{ box-sizing:border-box; }}
    body {{ margin:0; font-family:"IBM Plex Sans","Segoe UI",system-ui,sans-serif; color:var(--ink); background:linear-gradient(180deg,#faf7f2 0%,var(--bg) 100%); }}
    .shell {{ max-width:1380px; margin:0 auto; padding:28px 22px 60px; }}
    .hero {{ background:linear-gradient(135deg,rgba(116,0,0,.92),rgba(155,34,38,.92)); color:#fff9f9; border-radius:24px; padding:28px 30px; box-shadow:var(--shadow); }}
    .hero h1 {{ margin:0 0 12px; font-size:clamp(28px,4vw,42px); line-height:1.05; }}
    .hero-meta {{ color:rgba(255,249,249,.82); font-size:14px; }}
    .hero-links {{ display:flex; gap:10px; flex-wrap:wrap; margin-top:14px; }}
    .hero-links a {{ text-decoration:none; color:#fff9f9; border:1px solid rgba(255,249,249,.28); padding:9px 12px; border-radius:999px; font-size:14px; }}
    .grid {{ display:grid; grid-template-columns:repeat(12,minmax(0,1fr)); gap:18px; margin-top:22px; }}
    .panel {{ background:var(--paper); border:1px solid var(--line); border-radius:22px; padding:22px; box-shadow:var(--shadow); }}
    .span-12 {{ grid-column:span 12; }} .span-6 {{ grid-column:span 6; }}
    .stack {{ display:grid; gap:14px; }}
    .stack-card {{ border:1px solid var(--line); border-radius:18px; padding:16px 18px; background:#fffdfa; }}
    .stack-card-head {{ display:flex; justify-content:space-between; align-items:center; gap:12px; margin-bottom:10px; }}
    .stack-card-body,.stack-card-action {{ margin:0 0 10px; line-height:1.55; }}
    .badge {{ display:inline-flex; align-items:center; justify-content:center; padding:6px 10px; border-radius:999px; font-size:12px; font-weight:700; text-transform:uppercase; letter-spacing:.06em; color:#fff; }}
    .badge-critical {{ background:var(--critical); }} .badge-high {{ background:var(--high); }} .badge-medium {{ background:var(--medium); }} .badge-low {{ background:var(--low); }} .badge-none {{ background:var(--none); }}
    .inline-link {{ color:var(--accent); text-decoration:none; font-weight:600; }} .inline-link:hover {{ text-decoration:underline; }}
    .meta-list {{ margin:0; padding-left:18px; line-height:1.55; }}
    @media (max-width:1100px) {{ .span-6 {{ grid-column:span 12; }} }}
    @media (max-width:640px) {{ .shell {{ padding:16px 14px 40px; }} .hero {{ padding:22px 18px; }} }}
  </style>
</head>
<body>
  <main class="shell">
    <section class="hero">
      <div class="hero-meta">AW-rus · AI Recovery Brief · render mode: {html.escape(str(render_mode))}</div>
      <h1>{html.escape(str(recovery.get('headline') or 'Recovery brief недоступен'))}</h1>
      <div class="hero-meta">Сформировано: {html.escape(str(generated_at or '-'))}</div>
      <nav class="hero-links">
        <a href="/manager/brief">Текущий brief</a>
        <a href="/manager/changes">Что изменилось</a>
        <a href="/manager/trends/weekly">Неделя</a>
        <a href="/manager/digest/weekly">Weekly digest</a>
        <a href="/manager/problematic?days=7">Проблемные 7д</a>
        <a href="/api/1/analytics-1c/manager/recovery/latest">JSON</a>
        <a href="/api/1/analytics-1c/manager/recovery/latest.md">Markdown</a>
      </nav>
    </section>
    <section class="grid">
      <article class="panel span-6">
        <h2>Ситуация</h2>
        <ul class="meta-list">{situation_items or '<li>Нет данных.</li>'}</ul>
      </article>
      <article class="panel span-6">
        <h2>Что делать по портфелю</h2>
        <ul class="meta-list">{action_items or '<li>Нет данных.</li>'}</ul>
      </article>
      <article class="panel span-12">
        <h2>Компании первой очереди для recovery</h2>
        <div class="stack">{''.join(incident_cards) or '<p>Нет recovery-инцидентов.</p>'}</div>
      </article>
      <article class="panel span-12">
        <h2>Ограничения интерпретации</h2>
        <ul class="meta-list">{caveat_items or '<li>Нет данных.</li>'}</ul>
      </article>
    </section>
  </main>
</body>
</html>"""


app = FastAPI(title="AW-rus 1C Company Intelligence API", version="1.0.0")


@app.get("/")
def root() -> Response:
    return Response(status_code=307, headers={"Location": "/manager/brief"})


@app.get("/favicon.ico")
def favicon() -> Response:
    return Response(status_code=204)


@app.get("/health")
def health() -> dict[str, Any]:
    client = ch_client()
    summary = rows_to_dict(
        client.query(
            """
            SELECT
                countIf(counterparty != '') AS documents_with_counterparty,
                (SELECT count() FROM analytics_1c.companies) AS companies_total,
                (SELECT count() FROM analytics_1c.company_registry) AS registry_rows_total,
                (SELECT count() FROM analytics_1c.company_forecasts) AS forecasts_total,
                (SELECT count() FROM analytics_1c.company_health_signals) AS health_signals_total
            FROM analytics_1c.documents
            """
        )
    )[0]
    return {"status": "ok", "generated_at": datetime.now(UTC).isoformat(), **summary}


@app.get("/api/health")
def api_health() -> dict[str, Any]:
    return health()


@app.get("/api/1/analytics-1c/companies/overview")
def companies_overview(
    infobase: str | None = None,
    min_signal_score: int = Query(default=0, ge=0, le=100),
    limit: int = Query(default=50, ge=1, le=500),
) -> dict[str, Any]:
    client = ch_client()
    where = [f"signal_score >= {int(min_signal_score)}"]
    if infobase:
        where.append(f"infobase = {q(infobase)}")
    sql = f"""
    SELECT
        infobase,
        company_entity_key,
        organization,
        counterparty,
        source_counterparty,
        company_name,
        normalized_counterparty,
        registry_match_mode,
        registry_company_key,
        registry_binding_source,
        registry_binding_note,
        registry_assignee_name,
        registry_status,
        registry_share_text,
        registry_key_contour,
        registry_inn,
        registry_kpp,
        owner_user,
        base_path,
        current_status,
        db_size_bytes,
        reglog_size_bytes,
        active_locks,
        current_activity_score,
        last_seen_at,
        days_since_last_activity,
        docs_7d,
        amount_7d,
        docs_30d,
        amount_30d,
        amount_forecast_30d,
        docs_forecast_30d,
        signal_severity,
        signal_score,
        top_signal
    FROM analytics_1c.v_company_portfolio_overview
    WHERE {' AND '.join(where)}
    ORDER BY signal_score DESC, amount_30d DESC, counterparty
    LIMIT {int(limit)}
    """
    rows = rows_to_dict(client.query(sql))
    return {"items": rows, "count": len(rows)}


@app.get("/api/1/analytics-1c/companies/{counterparty}/summary")
def company_summary(counterparty: str, infobase: str | None = None) -> dict[str, Any]:
    client = ch_client()
    card = resolve_company_portfolio_card(counterparty, infobase)
    entity_key = str(card.get("company_entity_key") or "")
    forecast_sql = f"""
    SELECT metric, horizon_days, baseline_daily, trend_slope, predicted_daily, predicted_total, confidence, note
    FROM analytics_1c.v_company_forecasts_current
    WHERE counterparty = {q(entity_key)}
    {"AND infobase = " + q(str(card.get('infobase') or infobase)) if (card.get('infobase') or infobase) else ""}
    ORDER BY metric, horizon_days
    """
    signals_sql = f"""
    SELECT generated_at, severity, score, signal_type, summary
    FROM analytics_1c.v_company_health_current
    WHERE counterparty = {q(entity_key)}
    {"AND infobase = " + q(str(card.get('infobase') or infobase)) if (card.get('infobase') or infobase) else ""}
    ORDER BY score DESC, generated_at DESC
    """
    timeline_sql = f"""
    SELECT last_company_snapshot_at AS ts, infobase, company_name, owner_user, current_status, db_size_bytes, reglog_size_bytes, active_locks, current_activity_score
    FROM analytics_1c.v_company_portfolio_overview
    WHERE company_entity_key = {q(entity_key)}
    {"AND infobase = " + q(str(card.get('infobase') or infobase)) if (card.get('infobase') or infobase) else ""}
    ORDER BY ts DESC
    LIMIT 1
    """
    forecasts = rows_to_dict(client.query(forecast_sql))
    signals = rows_to_dict(client.query(signals_sql))
    company_state = rows_to_dict(client.query(timeline_sql))
    timeline_sql = f"""
    SELECT ts, infobase, doc_type, operation_type, amount, status, author
    FROM analytics_1c.documents
    WHERE infobase = {q(str(card.get('infobase') or infobase or ''))}
      AND counterparty != ''
    ORDER BY ts DESC
    LIMIT 20
    """
    timeline = rows_to_dict(client.query(timeline_sql))
    essence = (
        f"Компания {card['counterparty']}: за 30 дней событий {card['docs_30d']}, суммарная активность {card['amount_30d']}, "
        f"прогноз активности на 30 дней {card['amount_forecast_30d']}, риск {card['signal_severity']}."
    )
    payload = {
        "essence": essence,
        "card": card,
        "company_state": company_state[0] if company_state else None,
        "forecasts": forecasts,
        "signals": signals,
        "recent_documents": timeline,
        "management_actions": company_management_actions(counterparty, infobase, limit=10),
    }
    payload["priority_context"] = build_company_priority_context(payload, infobase)
    payload["recovery_context"] = build_company_recovery_context(payload, infobase)
    return payload


@app.get("/api/1/analytics-1c/companies/{counterparty}/forecast")
def company_forecast(
    counterparty: str,
    infobase: str | None = None,
    horizon_days: int | None = Query(default=None, ge=1, le=365),
) -> dict[str, Any]:
    client = ch_client()
    card = resolve_company_portfolio_card(counterparty, infobase)
    filters = [f"counterparty = {q(str(card.get('company_entity_key') or counterparty))}"]
    if card.get("infobase") or infobase:
        filters.append(f"infobase = {q(str(card.get('infobase') or infobase))}")
    if horizon_days is not None:
        filters.append(f"horizon_days = {int(horizon_days)}")
    sql = f"""
    SELECT generated_at, infobase, counterparty, metric, horizon_days, baseline_daily, trend_slope, predicted_daily, predicted_total, confidence, note
    FROM analytics_1c.v_company_forecasts_current
    WHERE {' AND '.join(filters)}
    ORDER BY metric, horizon_days
    """
    rows = rows_to_dict(client.query(sql))
    return {"items": rows, "count": len(rows)}


@app.get("/api/1/analytics-1c/companies/{counterparty}/timeline")
def company_timeline(
    counterparty: str,
    infobase: str | None = None,
    limit: int = Query(default=100, ge=1, le=500),
) -> dict[str, Any]:
    client = ch_client()
    card = resolve_company_portfolio_card(counterparty, infobase)
    filters = [f"infobase = {q(str(card.get('infobase') or infobase or ''))}", "counterparty != ''"]
    sql = f"""
    SELECT ts, infobase, organization, doc_type, doc_number, author, operation_type, amount, status, posted
    FROM analytics_1c.documents
    WHERE {' AND '.join(filters)}
    ORDER BY ts DESC
    LIMIT {int(limit)}
    """
    rows = rows_to_dict(client.query(sql))
    return {"items": rows, "count": len(rows)}


@app.get("/api/1/analytics-1c/companies/{counterparty}/actions")
def company_actions(counterparty: str, infobase: str | None = None, limit: int = Query(default=20, ge=1, le=100)) -> dict[str, Any]:
    items = company_management_actions(counterparty, infobase, limit=limit)
    return {"items": items, "count": len(items)}


@app.get("/api/1/analytics-1c/manager/brief/latest")
def manager_brief_latest() -> dict[str, Any]:
    return load_latest_manager_brief()


@app.get("/api/1/analytics-1c/manager/brief/latest.md", response_class=PlainTextResponse)
def manager_brief_latest_markdown() -> str:
    latest_md = manager_brief_state_dir() / "latest.md"
    if not latest_md.exists():
        raise HTTPException(status_code=404, detail="manager brief markdown not generated yet")
    return latest_md.read_text(encoding="utf-8")


@app.get("/api/1/analytics-1c/manager/brief/history")
def manager_brief_history(limit: int = Query(default=20, ge=1, le=200)) -> dict[str, Any]:
    items = load_brief_history_records(limit)
    return {"items": items, "count": len(items)}


@app.get("/api/1/analytics-1c/manager/brief/history/{name}")
def manager_brief_history_record(name: str) -> dict[str, Any]:
    return load_brief_history_record(name)


@app.get("/api/1/analytics-1c/manager/brief/delta/latest")
def manager_brief_delta_latest() -> dict[str, Any]:
    payload = load_latest_manager_brief()
    delta = extract_delta(payload)
    return {
        "generated_at": payload.get("generated_at"),
        "headline": payload.get("brief", {}).get("headline"),
        "delta": delta,
    }


@app.get("/api/1/analytics-1c/manager/trends/weekly")
def manager_weekly_trends(days: int = Query(default=7, ge=2, le=30)) -> dict[str, Any]:
    return build_weekly_trend_report(load_brief_history_payloads(limit=400), days=days)


@app.get("/api/1/analytics-1c/manager/digest/weekly/latest")
def manager_weekly_digest_latest() -> dict[str, Any]:
    return load_latest_weekly_digest()


@app.get("/api/1/analytics-1c/manager/digest/weekly/latest.md", response_class=PlainTextResponse)
def manager_weekly_digest_latest_markdown() -> str:
    latest_md = weekly_digest_state_dir() / "latest.md"
    if not latest_md.exists():
        raise HTTPException(status_code=404, detail="weekly digest markdown not generated yet")
    return latest_md.read_text(encoding="utf-8")


@app.get("/api/1/analytics-1c/manager/recovery/latest")
def manager_recovery_latest() -> dict[str, Any]:
    return load_latest_recovery_brief()


@app.get("/api/1/analytics-1c/manager/recovery/latest.md", response_class=PlainTextResponse)
def manager_recovery_latest_markdown() -> str:
    latest_md = recovery_brief_state_dir() / "latest.md"
    if not latest_md.exists():
        raise HTTPException(status_code=404, detail="recovery brief markdown not generated yet")
    return latest_md.read_text(encoding="utf-8")


@app.get("/api/1/analytics-1c/manager/actions")
def manager_actions_api(
    priority_tier: str | None = Query(default=None),
    owner: str | None = Query(default=None),
    limit: int = Query(default=100, ge=1, le=500),
) -> dict[str, Any]:
    items = management_actions(priority_tier=priority_tier, owner=owner, limit=limit)
    return {
        "scope": "action_items",
        "priority_model": "enterprise_risk",
        "owner_mode": "secondary_operational_metadata",
        "items": items,
        "count": len(items),
        "priority_tier": priority_tier,
        "owner": owner,
    }


@app.get("/api/1/analytics-1c/manager/actions/companies")
def manager_company_actions_api(
    priority_tier: str | None = Query(default=None),
    owner: str | None = Query(default=None),
    limit: int = Query(default=100, ge=1, le=500),
) -> dict[str, Any]:
    items = management_company_actions(priority_tier=priority_tier, owner=owner, limit=limit)
    return {
        "scope": "companies",
        "priority_model": "enterprise_risk",
        "owner_mode": "secondary_operational_metadata",
        "items": items,
        "count": len(items),
        "priority_tier": priority_tier,
        "owner": owner,
    }


@app.get("/api/1/analytics-1c/companies/problematic")
def problematic_companies_api(
    days: int = Query(default=7, ge=1, le=30),
    limit: int = Query(default=50, ge=1, le=500),
) -> dict[str, Any]:
    items = problematic_companies(days=days, limit=limit)
    return {"items": items, "count": len(items), "days": days}


@app.get("/manager/brief", response_class=HTMLResponse)
def manager_brief_view() -> str:
    return render_manager_brief_html(load_latest_manager_brief())


@app.get("/manager/changes", response_class=HTMLResponse)
def manager_brief_delta_view() -> str:
    return render_brief_delta_html(load_latest_manager_brief())


@app.get("/manager/trends/weekly", response_class=HTMLResponse)
def manager_weekly_trends_view(days: int = Query(default=7, ge=2, le=30)) -> str:
    return render_weekly_trend_html(build_weekly_trend_report(load_brief_history_payloads(limit=400), days=days))


@app.get("/manager/digest/weekly", response_class=HTMLResponse)
def manager_weekly_digest_view() -> str:
    return render_weekly_digest_html(load_latest_weekly_digest())


@app.get("/manager/recovery", response_class=HTMLResponse)
def manager_recovery_view() -> str:
    return render_recovery_brief_html(load_latest_recovery_brief())


@app.get("/manager/actions", response_class=HTMLResponse)
def manager_actions_view(
    priority_tier: str | None = Query(default=None),
    owner: str | None = Query(default=None),
    limit: int = Query(default=100, ge=1, le=500),
) -> str:
    return render_management_actions_html(
        management_actions(priority_tier=priority_tier, owner=owner, limit=limit),
        management_company_actions(priority_tier=priority_tier, owner=owner, limit=limit),
        priority_tier=priority_tier,
        owner=owner,
    )


@app.get("/manager/briefs", response_class=HTMLResponse)
def manager_brief_history_view(limit: int = Query(default=40, ge=1, le=200)) -> str:
    return render_brief_history_html(load_brief_history_records(limit))


@app.get("/manager/briefs/{name}", response_class=HTMLResponse)
def manager_brief_history_detail_view(name: str) -> str:
    return render_manager_brief_html(load_brief_history_record(name))


@app.get("/manager/briefs/{name}/changes", response_class=HTMLResponse)
def manager_brief_history_delta_view(name: str) -> str:
    return render_brief_delta_html(load_brief_history_record(name))


@app.get("/manager/problematic", response_class=HTMLResponse)
def manager_problematic_companies_view(
    days: int = Query(default=7, ge=1, le=30),
    limit: int = Query(default=50, ge=1, le=500),
) -> str:
    return render_problematic_companies_html(problematic_companies(days=days, limit=limit), days)


def render_company_detail_html(summary_payload: dict[str, Any], infobase: str | None = None) -> str:
    card = summary_payload["card"]
    company_state = summary_payload.get("company_state") or {}
    forecasts = summary_payload.get("forecasts") or []
    signals = summary_payload.get("signals") or []
    recent_documents = summary_payload.get("recent_documents") or []
    management_actions_payload = summary_payload.get("management_actions") or []
    priority_context = summary_payload.get("priority_context") or build_company_priority_context(summary_payload, infobase)
    recovery_context = summary_payload.get("recovery_context") or build_company_recovery_context(summary_payload, infobase)

    title = card.get("counterparty", "Карточка компании")
    subtitle = summary_payload.get("essence", "")
    grafana_url = grafana_company_dashboard_url()
    company_ref = str(card.get("company_entity_key") or card.get("counterparty") or "")
    summary_url = f"/api/1/analytics-1c/companies/{quote(company_ref)}/summary"
    if infobase:
        summary_url += f"?infobase={quote(infobase)}"
    timeline_url = f"/api/1/analytics-1c/companies/{quote(company_ref)}/timeline"
    if infobase:
        timeline_url += f"?infobase={quote(infobase)}"
    forecast_url = f"/api/1/analytics-1c/companies/{quote(company_ref)}/forecast"
    if infobase:
        forecast_url += f"?infobase={quote(infobase)}"
    actions_url = f"/api/1/analytics-1c/companies/{quote(company_ref)}/actions"
    if infobase:
        actions_url += f"?infobase={quote(infobase)}"

    if card.get("signal_severity") == "critical":
        manager_comment = "Компания требует немедленного внимания: контур считает её operational-critical."
    elif card.get("signal_severity") == "high":
        manager_comment = "Компания в зоне повышенного внимания: нужен короткий управленческий разбор причин сигнала."
    elif card.get("signal_severity") == "medium":
        manager_comment = "Компания не аварийная, но требует точечной проверки причин отклонения."
    else:
        manager_comment = "По компании нет выраженного аварийного сигнала; смотреть контекст и тренд активности."

    metrics = [
        ("Активность 7д", fmt_number(card.get("amount_7d"))),
        ("Активность 30д", fmt_number(card.get("amount_30d"))),
        ("Прогноз 30д", fmt_number(card.get("amount_forecast_30d"))),
        ("Документы 30д", fmt_number(card.get("docs_30d"))),
        ("Кейсы", fmt_number(card.get("open_cases_total"))),
        ("Detections", fmt_number(card.get("detections_total"))),
    ]
    metric_cards = "".join(
        f"<div class=\"stat\"><div class=\"stat-label\">{html.escape(label)}</div><div class=\"stat-value\">{html.escape(value)}</div></div>"
        for label, value in metrics
    )

    evidence_items = "".join(
        f"<li>{html.escape(str(item))}</li>"
        for item in priority_context.get("evidence", [])
    )
    action_items = "".join(
        f"<li>{html.escape(str(item))}</li>"
        for item in priority_context.get("actions", [])
    )
    recovery_action_items = "".join(
        f"<li>{html.escape(str(item))}</li>"
        for item in recovery_context.get("actions", [])
    )
    management_action_items = []
    for item in management_actions_payload[:6]:
        management_action_items.append(
            "<li>"
            f"{severity_badge(str(item.get('priority_tier') or 'low'))} "
            f"<strong>{html.escape(str(item.get('recommended_action') or '-'))}</strong> "
            f"({html.escape(str(item.get('deadline_hint') or '-'))})"
            f"<br><span>{html.escape(str(item.get('reason') or '-'))}</span>"
            "</li>"
        )

    forecast_rows = []
    for item in forecasts:
        forecast_rows.append(
            "<tr>"
            f"<td>{html.escape(str(item.get('metric', '-')))}</td>"
            f"<td>{fmt_number(item.get('horizon_days'))}</td>"
            f"<td>{fmt_number(item.get('baseline_daily'))}</td>"
            f"<td>{fmt_number(item.get('predicted_total'))}</td>"
            f"<td>{fmt_number(item.get('confidence'))}</td>"
            f"<td>{html.escape(str(item.get('note', '-')))}</td>"
            "</tr>"
        )

    signal_rows = []
    for item in signals[:12]:
        signal_rows.append(
            "<tr>"
            f"<td>{html.escape(str(item.get('generated_at', '-')))}</td>"
            f"<td>{severity_badge(str(item.get('severity', 'none')))}</td>"
            f"<td>{fmt_number(item.get('score'))}</td>"
            f"<td>{html.escape(str(item.get('signal_type', '-')))}</td>"
            f"<td>{html.escape(str(item.get('summary', '-')))}</td>"
            "</tr>"
        )

    document_rows = []
    for item in recent_documents[:20]:
        document_rows.append(
            "<tr>"
            f"<td>{html.escape(str(item.get('ts', '-')))}</td>"
            f"<td>{html.escape(str(item.get('doc_type', '-')))}</td>"
            f"<td>{html.escape(str(item.get('operation_type', '-')))}</td>"
            f"<td>{fmt_number(item.get('amount'))}</td>"
            f"<td>{html.escape(str(item.get('status', '-')))}</td>"
            f"<td>{html.escape(str(item.get('author', '-')))}</td>"
            "</tr>"
        )

    registry_comment = (
        f"Сопоставление с реестром: {card.get('registry_match_mode', 'none')}. "
        f"Ответственный: {card.get('registry_assignee_name') or 'не указан'}."
    )
    if card.get("registry_match_mode") == "manual":
        registry_comment += " Требуется осторожность: запись заведена через manual override."

    return f"""<!doctype html>
<html lang="ru">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta http-equiv="refresh" content="300">
  <title>{html.escape(title)} · 1C Company Brief</title>
  <style>
    :root {{
      --bg: #f4f1ea;
      --paper: #fffdf8;
      --ink: #1c1a17;
      --muted: #6b655c;
      --line: #d8d0c4;
      --accent: #005f73;
      --critical: #9b2226;
      --high: #bb3e03;
      --medium: #ca6702;
      --low: #4d7c0f;
      --none: #687076;
      --shadow: 0 14px 40px rgba(28, 26, 23, 0.08);
    }}
    * {{ box-sizing: border-box; }}
    body {{
      margin: 0;
      font-family: "IBM Plex Sans", "Segoe UI", system-ui, sans-serif;
      color: var(--ink);
      background:
        radial-gradient(circle at top left, rgba(0, 95, 115, 0.08), transparent 28%),
        linear-gradient(180deg, #faf7f2 0%, var(--bg) 100%);
    }}
    .shell {{ max-width: 1380px; margin: 0 auto; padding: 28px 22px 60px; }}
    .hero {{
      background: linear-gradient(135deg, rgba(0,95,115,0.94), rgba(10,77,104,0.92));
      color: #f8fbfc;
      border-radius: 24px;
      padding: 28px 30px;
      box-shadow: var(--shadow);
    }}
    .hero-top {{
      display: flex; justify-content: space-between; gap: 20px; align-items: flex-start; flex-wrap: wrap;
    }}
    .hero h1 {{ margin: 0 0 12px; font-size: clamp(28px, 4vw, 42px); line-height: 1.05; }}
    .hero-meta {{ color: rgba(248, 251, 252, 0.82); font-size: 14px; }}
    .hero-links {{ display: flex; gap: 10px; flex-wrap: wrap; }}
    .hero-links a {{
      text-decoration: none; color: #f8fbfc; border: 1px solid rgba(248, 251, 252, 0.28);
      padding: 9px 12px; border-radius: 999px; font-size: 14px;
    }}
    .hero-links a:hover {{ background: rgba(248, 251, 252, 0.12); }}
    .grid {{ display: grid; grid-template-columns: repeat(12, minmax(0, 1fr)); gap: 18px; margin-top: 22px; }}
    .panel {{ background: var(--paper); border: 1px solid var(--line); border-radius: 22px; padding: 22px; box-shadow: var(--shadow); }}
    .panel h2 {{ margin: 0 0 14px; font-size: 21px; line-height: 1.1; }}
    .span-12 {{ grid-column: span 12; }}
    .span-8 {{ grid-column: span 8; }}
    .span-6 {{ grid-column: span 6; }}
    .span-4 {{ grid-column: span 4; }}
    .summary-box {{
      border: 1px solid var(--line); border-left: 5px solid var(--accent); border-radius: 18px; padding: 16px 18px;
      background: #fffdfa; line-height: 1.55;
    }}
    .summary-box-priority {{
      border-left-color: var(--critical);
      background: #fff8f8;
    }}
    .stats {{ display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; }}
    .stat {{ padding: 16px; border-radius: 18px; background: linear-gradient(180deg, #fff 0%, #f7f4ee 100%); border: 1px solid var(--line); }}
    .stat-label {{ color: var(--muted); font-size: 13px; margin-bottom: 8px; }}
    .stat-value {{ font-size: 24px; font-weight: 700; letter-spacing: -0.03em; }}
    .badge {{
      display: inline-flex; align-items: center; justify-content: center; padding: 6px 10px;
      border-radius: 999px; font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: #fff;
    }}
    .badge-critical {{ background: var(--critical); }}
    .badge-high {{ background: var(--high); }}
    .badge-medium {{ background: var(--medium); }}
    .badge-low {{ background: var(--low); }}
    .badge-none {{ background: var(--none); }}
    table {{ width: 100%; border-collapse: collapse; font-size: 14px; }}
    th, td {{ text-align: left; padding: 10px 8px; border-bottom: 1px solid var(--line); vertical-align: top; }}
    th {{ color: var(--muted); font-weight: 600; font-size: 12px; text-transform: uppercase; letter-spacing: 0.06em; }}
    .inline-link {{ color: var(--accent); text-decoration: none; font-weight: 600; }}
    .inline-link:hover {{ text-decoration: underline; }}
    .meta-list {{ margin: 0; padding-left: 18px; line-height: 1.55; }}
    @media (max-width: 1100px) {{
      .span-8, .span-6, .span-4 {{ grid-column: span 12; }}
      .stats {{ grid-template-columns: repeat(2, minmax(0, 1fr)); }}
    }}
    @media (max-width: 640px) {{
      .shell {{ padding: 16px 14px 40px; }}
      .hero {{ padding: 22px 18px; }}
      .stats {{ grid-template-columns: 1fr; }}
      th:nth-child(2), td:nth-child(2) {{ display: none; }}
    }}
  </style>
</head>
<body>
  <main class="shell">
    <section class="hero">
      <div class="hero-top">
        <div>
          <div class="hero-meta">AW-rus · Company Brief · {severity_badge(str(card.get("signal_severity", "none")))}</div>
          <h1>{html.escape(title)}</h1>
          <div class="hero-meta">{html.escape(subtitle)}</div>
        </div>
        <nav class="hero-links">
          <a href="/manager/brief">К портфелю</a>
          <a href="/manager/changes">Что изменилось</a>
          <a href="/manager/trends/weekly">Неделя</a>
          <a href="/manager/digest/weekly">Weekly digest</a>
          <a href="/manager/recovery">AI recovery</a>
          <a href="/manager/actions">Очередь действий</a>
          <a href="/manager/briefs">История brief</a>
          <a href="/manager/problematic?days=7">Проблемные компании</a>
          <a href="{html.escape(summary_url)}">JSON summary</a>
          <a href="{html.escape(forecast_url)}">JSON forecast</a>
          <a href="{html.escape(timeline_url)}">JSON timeline</a>
          <a href="{html.escape(actions_url)}">JSON actions</a>
          <a href="{html.escape(grafana_url)}">Grafana</a>
        </nav>
      </div>
    </section>

    <section class="grid">
      <article class="panel span-8">
        <h2>Управленческий комментарий</h2>
        <div class="summary-box">
          <p><strong>{html.escape(manager_comment)}</strong></p>
          <p>{html.escape(registry_comment)}</p>
          <p>Сейчас база в состоянии <strong>{html.escape(str(card.get("current_status", "-")))}</strong>, активных блокировок: <strong>{fmt_number(card.get("active_locks"))}</strong>, дней с последней активности: <strong>{fmt_number(card.get("days_since_last_activity"))}</strong>.</p>
        </div>
      </article>

      <article class="panel span-4">
        <h2>Ключевые метрики</h2>
        <div class="stats">
          {metric_cards}
        </div>
      </article>

      <article class="panel span-8">
        <h2>Почему приоритет высокий именно у этой компании</h2>
        <div class="summary-box summary-box-priority">
          <p><strong>{html.escape(str(priority_context.get("verdict") or "-"))}</strong></p>
          <p>Текущий приоритет: {severity_badge(str(priority_context.get("current_priority_tier") or "low"))}
          · score {html.escape(fmt_number(priority_context.get("current_priority_score")))}
          · причина: {html.escape(str(priority_context.get("current_priority_reason") or "-"))}</p>
          <ul class="meta-list">{evidence_items or '<li>Жёстких драйверов приоритета не зафиксировано.</li>'}</ul>
        </div>
      </article>

      <article class="panel span-4">
        <h2>Что проверить первым действием</h2>
        <ul class="meta-list">{action_items}</ul>
      </article>

      <article class="panel span-12">
        <h2>Текущие управленческие действия по компании</h2>
        <ul class="meta-list">{''.join(management_action_items) or '<li>По компании пока нет отдельных строк в action queue.</li>'}</ul>
      </article>

      <article class="panel span-12">
        <h2>AI-план снятия проблемы</h2>
        <div class="summary-box summary-box-priority">
          <p><strong>Диагноз:</strong> {html.escape(str(recovery_context.get("diagnosis") or "-"))}</p>
          <p><strong>Что не делать:</strong> {html.escape(str(recovery_context.get("stop_doing") or "-"))}</p>
          <p><strong>Цель 24ч:</strong> {html.escape(str(recovery_context.get("target_state_24h") or "-"))}</p>
          <p><strong>Источник:</strong> {html.escape(str(recovery_context.get("confidence") or "-"))}
          {f" · сформировано {html.escape(str(recovery_context.get('generated_at')))}" if recovery_context.get("generated_at") else ""}</p>
          <ul class="meta-list">{recovery_action_items or '<li>Нет действий.</li>'}</ul>
        </div>
      </article>

      <article class="panel span-6">
        <h2>Карточка компании</h2>
        <ul class="meta-list">
          <li><strong>Компания:</strong> {html.escape(str(card.get("company_name") or card.get("counterparty") or "-"))}</li>
          <li><strong>Нормализованное имя:</strong> {html.escape(str(card.get("normalized_counterparty") or "-"))}</li>
          <li><strong>Инфобаза:</strong> {html.escape(str(card.get("infobase") or "-"))}</li>
          <li><strong>Ответственный:</strong> {html.escape(str(card.get("registry_assignee_name") or card.get("owner_user") or "-"))}</li>
          <li><strong>Match mode:</strong> {html.escape(str(card.get("registry_match_mode") or "-"))}</li>
          <li><strong>ИНН / КПП:</strong> {html.escape(str(card.get("registry_inn") or "-"))} / {html.escape(str(card.get("registry_kpp") or "-"))}</li>
          <li><strong>Путь базы:</strong> {html.escape(str(card.get("base_path") or "-"))}</li>
        </ul>
      </article>

      <article class="panel span-6">
        <h2>Состояние файловой базы</h2>
        <ul class="meta-list">
          <li><strong>Статус:</strong> {html.escape(str(company_state.get("current_status") or card.get("current_status") or "-"))}</li>
          <li><strong>Размер базы:</strong> {fmt_number(company_state.get("db_size_bytes") or card.get("db_size_bytes"))}</li>
          <li><strong>Размер reglog:</strong> {fmt_number(company_state.get("reglog_size_bytes") or card.get("reglog_size_bytes"))}</li>
          <li><strong>Активные блокировки:</strong> {fmt_number(company_state.get("active_locks") or card.get("active_locks"))}</li>
          <li><strong>Текущий activity score:</strong> {fmt_number(company_state.get("current_activity_score") or card.get("current_activity_score"))}</li>
          <li><strong>Последний snapshot:</strong> {html.escape(str(company_state.get("ts") or card.get("last_company_snapshot_at") or "-"))}</li>
        </ul>
      </article>

      <article class="panel span-12">
        <h2>Прогнозы</h2>
        <table>
          <thead>
            <tr>
              <th>Метрика</th>
              <th>Горизонт, д</th>
              <th>Baseline</th>
              <th>Прогноз total</th>
              <th>Confidence</th>
              <th>Примечание</th>
            </tr>
          </thead>
          <tbody>
            {''.join(forecast_rows) or '<tr><td colspan="6">Нет данных.</td></tr>'}
          </tbody>
        </table>
      </article>

      <article class="panel span-12">
        <h2>Сигналы</h2>
        <table>
          <thead>
            <tr>
              <th>Время</th>
              <th>Severity</th>
              <th>Score</th>
              <th>Тип</th>
              <th>Комментарий</th>
            </tr>
          </thead>
          <tbody>
            {''.join(signal_rows) or '<tr><td colspan="5">Нет данных.</td></tr>'}
          </tbody>
        </table>
      </article>

      <article class="panel span-12">
        <h2>Последние события</h2>
        <table>
          <thead>
            <tr>
              <th>Время</th>
              <th>Тип</th>
              <th>Операция</th>
              <th>Активность</th>
              <th>Статус</th>
              <th>Автор</th>
            </tr>
          </thead>
          <tbody>
            {''.join(document_rows) or '<tr><td colspan="6">Нет данных.</td></tr>'}
          </tbody>
        </table>
      </article>
    </section>
  </main>
</body>
</html>"""


@app.get("/manager/company/{counterparty}", response_class=HTMLResponse)
def manager_company_view(counterparty: str, infobase: str | None = None) -> str:
    return render_company_detail_html(company_summary(counterparty, infobase), infobase)


if __name__ == "__main__":
    args = parse_args()
    uvicorn.run(app, host=args.host, port=args.port)
