#!/usr/bin/env python3
"""
build_reminders.py - 根据结构化合同要点生成提醒 schema

用途：
1. 不直接创建自动提醒任务
2. 仅根据合同审查阶段提取出的结构化字段，生成 reminders JSON
3. 供后续 skill 归档到输出目录，或由其他流程继续消费

典型用法：
  python3 build_reminders.py --input /path/to/contract_review.json
  python3 build_reminders.py --input /path/to/contract_review.json --output /path/to/contract_reminders.json
  cat /path/to/contract_review.json | python3 build_reminders.py --stdin

推荐输入格式（字段允许缺省）：
{
  "contractName": "软件开发服务合同",
  "contractId": "CTR-2026-001",
  "counterparty": "某某科技有限公司",
  "currency": "CNY",
  "signedDate": "2026-03-31",
  "effectiveDate": "2026-04-01",
  "expireDate": "2027-03-31",
  "terminationNoticeDays": 30,
  "leadDays": {
    "expiry": [30, 7, 1, 0],
    "payment": [7, 3, 1, 0],
    "breach": [3, 1, 0]
  },
  "renewal": {
    "autoRenew": true,
    "noticeDays": 30,
    "sourceClause": "合同期限第10.2条"
  },
  "payments": [
    {
      "title": "首付款",
      "direction": "payable",
      "amount": "100000",
      "currency": "CNY",
      "dueDate": "2026-04-10",
      "triggerCondition": "合同签订后5日内支付",
      "sourceClause": "付款条款第2.1条"
    },
    {
      "title": "尾款",
      "direction": "receivable",
      "triggerCondition": "验收合格后10日内支付",
      "sourceClause": "付款条款第2.3条"
    }
  ],
  "breachRules": [
    {
      "title": "逾期付款违约风险",
      "riskType": "late_payment",
      "severity": "high",
      "triggerRule": "应付款到期后超过10日未支付",
      "graceDays": 10,
      "relatedPaymentTitle": "首付款",
      "sourceClause": "违约责任第6.2条"
    }
  ]
}

输出结构：
{
  "schemaVersion": "1.0",
  "generatedAt": "...",
  "contract": {...},
  "expiryReminders": [...],
  "paymentReminders": [...],
  "breachRiskAlerts": [...],
  "summary": {...},
  "warnings": [...]
}
"""

from __future__ import annotations

import argparse
import json
import sys
from datetime import date, datetime, timedelta
from pathlib import Path
from typing import Any


DEFAULT_EXPIRY_LEADS = [30, 7, 1, 0]
DEFAULT_PAYMENT_LEADS = [7, 3, 1, 0]
DEFAULT_BREACH_LEADS = [3, 1, 0]


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description="根据结构化合同信息生成提醒 schema")
    parser.add_argument("--input", help="输入 JSON 文件路径")
    parser.add_argument("--stdin", action="store_true", help="从 stdin 读取 JSON")
    parser.add_argument("--output", help="输出 JSON 文件路径；不传则打印到 stdout")
    parser.add_argument("--pretty", action="store_true", help="强制格式化输出（默认开启）")
    return parser.parse_args()


def fail(message: str, exit_code: int = 1) -> None:
    print(message, file=sys.stderr)
    raise SystemExit(exit_code)


def read_payload(args: argparse.Namespace) -> dict[str, Any]:
    if args.stdin:
        raw = sys.stdin.read()
    elif args.input:
        input_path = Path(args.input).expanduser()
        if not input_path.exists():
            fail(f"输入文件不存在: {input_path}")
        raw = input_path.read_text(encoding="utf-8")
    else:
        fail("请指定 --input 或 --stdin")

    try:
        payload = json.loads(raw)
    except json.JSONDecodeError as exc:
        fail(f"输入 JSON 解析失败: {exc}")

    if not isinstance(payload, dict):
        fail("输入 JSON 顶层必须是对象")
    return payload


def normalize_string(value: Any) -> str | None:
    if value is None:
        return None
    text = str(value).strip()
    return text or None


def normalize_int(value: Any) -> int | None:
    if value is None or value == "":
        return None
    if isinstance(value, bool):
        return None
    try:
        return int(str(value).strip())
    except (TypeError, ValueError):
        return None


def parse_iso_date(value: Any) -> date | None:
    text = normalize_string(value)
    if not text:
        return None
    for fmt in ("%Y-%m-%d", "%Y/%m/%d", "%Y-%m-%dT%H:%M:%S", "%Y-%m-%d %H:%M:%S"):
        try:
            return datetime.strptime(text, fmt).date()
        except ValueError:
            continue
    try:
        return datetime.fromisoformat(text.replace("Z", "+00:00")).date()
    except ValueError:
        return None


def format_date(value: date | None) -> str | None:
    return value.isoformat() if value else None


def unique_sorted_ints(values: list[Any], defaults: list[int]) -> list[int]:
    parsed: list[int] = []
    for value in values:
        number = normalize_int(value)
        if number is None or number < 0:
            continue
        parsed.append(number)
    if not parsed:
        parsed = list(defaults)
    return sorted(set(parsed), reverse=True)


def resolve_lead_days(payload: dict[str, Any]) -> dict[str, list[int]]:
    lead_days = payload.get("leadDays")
    if not isinstance(lead_days, dict):
        lead_days = {}
    return {
        "expiry": unique_sorted_ints(lead_days.get("expiry", []), DEFAULT_EXPIRY_LEADS),
        "payment": unique_sorted_ints(lead_days.get("payment", []), DEFAULT_PAYMENT_LEADS),
        "breach": unique_sorted_ints(lead_days.get("breach", []), DEFAULT_BREACH_LEADS),
    }


def build_reminder_dates(anchor: date | None, lead_days: list[int]) -> list[str]:
    if not anchor:
        return []
    results: list[str] = []
    seen: set[str] = set()
    for days in lead_days:
        remind_on = anchor - timedelta(days=days)
        remind_text = remind_on.isoformat()
        if remind_text in seen:
            continue
        seen.add(remind_text)
        results.append(remind_text)
    return results


def normalize_direction(value: Any) -> str:
    text = (normalize_string(value) or "").lower()
    payable_values = {"payable", "out", "付款", "应付", "支付", "支出"}
    receivable_values = {"receivable", "in", "收款", "应收", "收入", "回款"}
    if text in payable_values:
        return "payable"
    if text in receivable_values:
        return "receivable"
    return "unknown"


def resolve_amount_text(item: dict[str, Any], default_currency: str | None) -> str | None:
    amount = normalize_string(item.get("amount"))
    currency = normalize_string(item.get("currency")) or default_currency
    if amount and currency:
        return f"{amount} {currency}"
    return amount or None


def build_expiry_reminders(
    payload: dict[str, Any],
    lead_days: dict[str, list[int]],
    warnings: list[str],
) -> list[dict[str, Any]]:
    reminders: list[dict[str, Any]] = []
    expire_date = parse_iso_date(payload.get("expireDate"))
    contract_name = normalize_string(payload.get("contractName")) or "未命名合同"

    if expire_date:
        reminders.append(
            {
                "kind": "contract_expiry",
                "title": f"{contract_name}到期提醒",
                "dueDate": format_date(expire_date),
                "leadDays": lead_days["expiry"],
                "suggestedReminderDates": build_reminder_dates(expire_date, lead_days["expiry"]),
                "sourceClause": normalize_string(payload.get("expirySourceClause")),
                "notes": "请在到期前复核续签、终止、结算、资料交接等事项。",
            }
        )
    else:
        warnings.append("未提供 expireDate，未生成合同到期提醒。")

    renewal = payload.get("renewal")
    if isinstance(renewal, dict) and expire_date:
        notice_days = normalize_int(renewal.get("noticeDays"))
        auto_renew = bool(renewal.get("autoRenew"))
        if auto_renew and notice_days is not None:
            deadline = expire_date - timedelta(days=notice_days)
            notice_leads = [day for day in [7, 1, 0] if day <= notice_days]
            if 0 not in notice_leads:
                notice_leads.append(0)
            notice_leads = sorted(set(notice_leads), reverse=True)
            reminders.append(
                {
                    "kind": "renewal_notice_deadline",
                    "title": f"{contract_name}续约/异议通知截止提醒",
                    "dueDate": format_date(deadline),
                    "leadDays": notice_leads,
                    "suggestedReminderDates": build_reminder_dates(deadline, notice_leads),
                    "sourceClause": normalize_string(renewal.get("sourceClause")),
                    "notes": f"合同约定到期前 {notice_days} 日通知；请在截止日前确认是否续签、终止或提出异议。",
                }
            )

    termination_notice_days = normalize_int(payload.get("terminationNoticeDays"))
    if expire_date and termination_notice_days is not None:
        deadline = expire_date - timedelta(days=termination_notice_days)
        notice_leads = [day for day in [7, 1, 0] if day <= termination_notice_days]
        if 0 not in notice_leads:
            notice_leads.append(0)
        notice_leads = sorted(set(notice_leads), reverse=True)
        reminders.append(
            {
                "kind": "termination_notice_deadline",
                "title": f"{contract_name}解约/终止通知截止提醒",
                "dueDate": format_date(deadline),
                "leadDays": notice_leads,
                "suggestedReminderDates": build_reminder_dates(deadline, notice_leads),
                "sourceClause": normalize_string(payload.get("terminationNoticeSourceClause")),
                "notes": f"合同涉及提前 {termination_notice_days} 日通知终止，请在截止日前完成内部决策与送达准备。",
            }
        )

    return reminders


def build_payment_reminders(
    payload: dict[str, Any],
    lead_days: dict[str, list[int]],
    warnings: list[str],
) -> tuple[list[dict[str, Any]], dict[str, dict[str, Any]]]:
    reminders: list[dict[str, Any]] = []
    payment_index: dict[str, dict[str, Any]] = {}
    contract_currency = normalize_string(payload.get("currency"))
    payments = payload.get("payments")

    if payments is None:
        payments = []
    if not isinstance(payments, list):
        warnings.append("payments 字段不是数组，已忽略。")
        payments = []

    for idx, raw_item in enumerate(payments, start=1):
        if not isinstance(raw_item, dict):
            warnings.append(f"payments[{idx}] 不是对象，已忽略。")
            continue

        title = normalize_string(raw_item.get("title")) or f"付款节点{idx}"
        direction = normalize_direction(raw_item.get("direction"))
        due_date = parse_iso_date(raw_item.get("dueDate"))
        amount_text = resolve_amount_text(raw_item, contract_currency)
        item = {
            "title": title,
            "type": direction,
            "amount": amount_text,
            "dueDate": format_date(due_date),
            "leadDays": lead_days["payment"] if due_date else [],
            "suggestedReminderDates": build_reminder_dates(due_date, lead_days["payment"]),
            "triggerCondition": normalize_string(raw_item.get("triggerCondition")),
            "sourceClause": normalize_string(raw_item.get("sourceClause")),
            "notes": normalize_string(raw_item.get("notes")),
        }

        if due_date is None and not item["triggerCondition"]:
            warnings.append(f"付款节点“{title}”缺少 dueDate 和 triggerCondition，仅保留为待确认项。")

        reminders.append(item)
        payment_index[title] = item

    return reminders, payment_index


def resolve_breach_anchor_date(
    rule: dict[str, Any],
    payment_index: dict[str, dict[str, Any]],
    payload: dict[str, Any],
) -> date | None:
    anchor_date = parse_iso_date(rule.get("anchorDate"))
    if anchor_date:
        return anchor_date

    related_payment_title = normalize_string(rule.get("relatedPaymentTitle"))
    if related_payment_title:
        payment = payment_index.get(related_payment_title)
        if payment:
            return parse_iso_date(payment.get("dueDate"))

    related_date_field = normalize_string(rule.get("relatedDateField"))
    if related_date_field:
        return parse_iso_date(payload.get(related_date_field))

    return None


def build_breach_risk_alerts(
    payload: dict[str, Any],
    lead_days: dict[str, list[int]],
    payment_index: dict[str, dict[str, Any]],
    warnings: list[str],
) -> list[dict[str, Any]]:
    alerts: list[dict[str, Any]] = []
    breach_rules = payload.get("breachRules")

    if breach_rules is None:
        breach_rules = []
    if not isinstance(breach_rules, list):
        warnings.append("breachRules 字段不是数组，已忽略。")
        breach_rules = []

    for idx, raw_rule in enumerate(breach_rules, start=1):
        if not isinstance(raw_rule, dict):
            warnings.append(f"breachRules[{idx}] 不是对象，已忽略。")
            continue

        title = normalize_string(raw_rule.get("title")) or f"违约风险预警{idx}"
        anchor_date = resolve_breach_anchor_date(raw_rule, payment_index, payload)
        grace_days = normalize_int(raw_rule.get("graceDays"))
        threshold_date: date | None = None
        if anchor_date and grace_days is not None:
            threshold_date = anchor_date + timedelta(days=grace_days)
        elif anchor_date:
            threshold_date = anchor_date

        if threshold_date is None:
            warnings.append(f"违约风险规则“{title}”缺少可计算日期锚点，仅输出规则说明。")

        alerts.append(
            {
                "title": title,
                "riskType": normalize_string(raw_rule.get("riskType")) or "general_breach",
                "severity": normalize_string(raw_rule.get("severity")) or "medium",
                "triggerRule": normalize_string(raw_rule.get("triggerRule")),
                "graceDays": grace_days,
                "anchorDate": format_date(anchor_date),
                "thresholdDate": format_date(threshold_date),
                "leadDays": lead_days["breach"] if threshold_date else [],
                "suggestedReminderDates": build_reminder_dates(threshold_date, lead_days["breach"]),
                "relatedPaymentTitle": normalize_string(raw_rule.get("relatedPaymentTitle")),
                "sourceClause": normalize_string(raw_rule.get("sourceClause")),
                "notes": (
                    "该项为违约风险预警，不代表已实际构成违约；"
                    "请结合履约事实、通知送达、补充协议等情况进一步核实。"
                ),
            }
        )

    return alerts


def build_contract_meta(payload: dict[str, Any]) -> dict[str, Any]:
    return {
        "contractName": normalize_string(payload.get("contractName")) or "未命名合同",
        "contractId": normalize_string(payload.get("contractId")),
        "counterparty": normalize_string(payload.get("counterparty")),
        "currency": normalize_string(payload.get("currency")),
        "signedDate": format_date(parse_iso_date(payload.get("signedDate"))),
        "effectiveDate": format_date(parse_iso_date(payload.get("effectiveDate"))),
        "expireDate": format_date(parse_iso_date(payload.get("expireDate"))),
        "archiveDir": normalize_string(payload.get("archiveDir")),
    }


def build_output(payload: dict[str, Any]) -> dict[str, Any]:
    warnings: list[str] = []
    lead_days = resolve_lead_days(payload)
    contract_meta = build_contract_meta(payload)
    expiry_reminders = build_expiry_reminders(payload, lead_days, warnings)
    payment_reminders, payment_index = build_payment_reminders(payload, lead_days, warnings)
    breach_risk_alerts = build_breach_risk_alerts(payload, lead_days, payment_index, warnings)

    return {
        "schemaVersion": "1.0",
        "generatedAt": datetime.now().isoformat(timespec="seconds"),
        "contract": contract_meta,
        "leadDays": lead_days,
        "expiryReminders": expiry_reminders,
        "paymentReminders": payment_reminders,
        "breachRiskAlerts": breach_risk_alerts,
        "summary": {
            "expiryReminderCount": len(expiry_reminders),
            "paymentReminderCount": len(payment_reminders),
            "breachRiskAlertCount": len(breach_risk_alerts),
            "warningCount": len(warnings),
        },
        "warnings": warnings,
    }


def write_output(result: dict[str, Any], output_path: str | None) -> None:
    text = json.dumps(result, ensure_ascii=False, indent=2)
    if output_path:
        target = Path(output_path).expanduser()
        target.parent.mkdir(parents=True, exist_ok=True)
        target.write_text(text + "\n", encoding="utf-8")
        return
    print(text)


def main() -> int:
    args = parse_args()
    payload = read_payload(args)
    result = build_output(payload)
    write_output(result, args.output)
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
