from __future__ import annotations

import json
import math
from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from matplotlib import font_manager
from matplotlib.colors import ListedColormap
from matplotlib.patches import FancyBboxPatch

ROOT = Path(__file__).resolve().parent
OUT = ROOT / "outputs"
FIG = OUT / "figures"
OUT.mkdir(exist_ok=True)
FIG.mkdir(exist_ok=True)

font_candidates = [
    ROOT.parents[1] / "public" / "fonts" / "NotoSansTC-Regular.ttf",
    Path("C:/Windows/Fonts/msjh.ttc"),
]
for candidate in font_candidates:
    if candidate.exists():
        font_manager.fontManager.addfont(str(candidate))
        plt.rcParams["font.family"] = font_manager.FontProperties(fname=str(candidate)).get_name()
        break
plt.rcParams["axes.unicode_minus"] = False
plt.rcParams["figure.dpi"] = 150

COLORS = {
    "brown": "#743c22",
    "orange": "#c9702c",
    "green": "#3f7551",
    "blue": "#547b91",
    "cream": "#f7efe4",
    "red": "#a6493f",
    "purple": "#765285",
    "grey": "#d7d2cc",
    "ink": "#2b211c",
}


def tags(value: object) -> set[str]:
    if pd.isna(value) or str(value).strip() == "":
        return set()
    return {item.strip() for item in str(value).split(";") if item.strip()}


programs = pd.read_csv(ROOT / "program-audit.csv")
profiles = pd.read_csv(ROOT / "company-profiles.csv")
risks = pd.read_csv(ROOT / "risk-matrix.csv")
safety = pd.read_csv(ROOT / "safety-tests.csv")

decision_fields = [
    ("eligibility_observed", 20, "申請資格"),
    ("deadline_observed", 15, "截止資訊"),
    ("max_support_observed", 15, "資源／金額上限"),
    ("cofunding_observed", 10, "配合款規則"),
    ("evaluation_observed", 15, "審查依據"),
    ("template_observed", 15, "申請模板"),
    ("contact_observed", 10, "聯絡窗口"),
]
equal_weights = {field: 100 / len(decision_fields) for field, _, _ in decision_fields}
finance_heavy_weights = {
    "eligibility_observed": 15,
    "deadline_observed": 10,
    "max_support_observed": 25,
    "cofunding_observed": 20,
    "evaluation_observed": 10,
    "template_observed": 10,
    "contact_observed": 10,
}

programs["document_readiness_weighted"] = sum(
    programs[field].astype(float) * weight for field, weight, _ in decision_fields
)
programs["document_readiness_equal"] = sum(
    programs[field].astype(float) * equal_weights[field] for field, _, _ in decision_fields
)
programs["document_readiness_finance_heavy"] = sum(
    programs[field].astype(float) * finance_heavy_weights[field] for field, _, _ in decision_fields
)
programs["unweighted_observable_rate"] = (
    programs[[field for field, _, _ in decision_fields]].sum(axis=1) / len(decision_fields)
)
programs[
    [
        "opportunity_id",
        "program_name",
        "track",
        "opportunity_form",
        "document_readiness_weighted",
        "document_readiness_equal",
        "document_readiness_finance_heavy",
        "unweighted_observable_rate",
    ]
].to_csv(OUT / "document-readiness.csv", index=False, encoding="utf-8-sig")

coverage_rows = []
for field, weight, label in decision_fields:
    coverage_rows.append(
        {
            "field": field,
            "label": label,
            "weight": weight,
            "observed_records": int(programs[field].sum()),
            "record_count": int(len(programs)),
            "observable_rate": float(programs[field].mean()),
        }
    )
coverage = pd.DataFrame(coverage_rows)
coverage.to_csv(OUT / "field-coverage.csv", index=False, encoding="utf-8-sig")


def preliminary_gate(program: pd.Series, profile: pd.Series) -> tuple[str, str]:
    conflict_reasons: list[str] = []
    pending_reasons: list[str] = []
    evidence_complete = profile["mandatory_evidence_status"] == "provided_in_scenario"

    if program["geography"] != "national" and program["geography"] != profile["geography"]:
        conflict_reasons.append("geography_mismatch")

    profile_applicant = tags(profile["applicant_tags"])
    profile_sector = tags(profile["sector_tags"])
    profile_special = tags(profile["special_tags"])

    required_any_applicant = tags(program["required_any_applicant_tags"])
    required_all_applicant = tags(program["required_all_applicant_tags"])
    required_any_sector = tags(program["required_any_sector_tags"])
    required_special = tags(program["required_special_tags"])

    def absent(reason: str) -> None:
        if evidence_complete:
            conflict_reasons.append(reason)
        else:
            pending_reasons.append(reason.replace("mismatch", "evidence_missing"))

    if required_any_applicant and not (required_any_applicant & profile_applicant):
        absent("applicant_type_mismatch")
    if required_all_applicant and not required_all_applicant.issubset(profile_applicant | profile_special):
        absent("required_applicant_tag_mismatch")
    if required_any_sector and not (required_any_sector & profile_sector):
        absent("sector_mismatch")
    if required_special and not required_special.issubset(profile_special | profile_applicant):
        absent("required_special_tag_mismatch")

    if conflict_reasons:
        return "known_conflict", ";".join(sorted(set(conflict_reasons)))
    if pending_reasons or not evidence_complete or program["eligibility_record_quality"] == "low":
        if program["eligibility_record_quality"] == "low":
            pending_reasons.append("source_detail_incomplete")
        if not evidence_complete:
            pending_reasons.append("enterprise_evidence_incomplete")
        return "pending_confirmation", ";".join(sorted(set(pending_reasons)))
    return (
        "conditional_pass",
        "modeled_conditions_match;final_official_and_enterprise_verification_required",
    )


def finance_fit(program: pd.Series, profile: pd.Series) -> tuple[float, str, float | None]:
    request = float(profile["planned_request_twd"])
    budget = float(profile["planned_project_budget_twd"])
    available = float(profile["available_cofunding_twd"])

    if int(program["max_support_observed"]) == 1 and not pd.isna(program["max_support_twd"]):
        if request > float(program["max_support_twd"]):
            return 0.0, "request_exceeds_observed_cap", None

    formula = str(program["cofunding_formula"])
    rate = float(program["cofunding_rate"]) if not pd.isna(program["cofunding_rate"]) else None
    if formula == "total_share" and rate is not None:
        required = budget * rate
        return (1.0 if available >= required else 0.0), (
            "cofunding_sufficient_in_scenario" if available >= required else "cofunding_shortfall_in_scenario"
        ), required
    if formula == "grant_share" and rate is not None:
        required = request * rate
        return (1.0 if available >= required else 0.0), (
            "cofunding_sufficient_in_scenario" if available >= required else "cofunding_shortfall_in_scenario"
        ), required
    if formula == "not_applicable":
        return 0.5, "non_cash_resource_not_comparable", None
    return 0.5, "financial_rule_not_observed", None


fit_rows: list[dict] = []
for _, profile in profiles.iterrows():
    profile_focus = tags(profile["focus_tags"])
    for _, program in programs.iterrows():
        program_focus = tags(program["focus_tags"])
        overlap = len(profile_focus & program_focus)
        union = len(profile_focus | program_focus)
        strategic = overlap / union if union else 0.0
        naive_shortlist = int(overlap > 0)
        gate_state, gate_reason = preliminary_gate(program, profile)
        finance_value, finance_state, minimum_cofunding = finance_fit(program, profile)
        score: float | None = None
        if gate_state == "conditional_pass":
            score = (
                strategic * 35
                + float(profile["execution_1_5"]) / 5 * 20
                + float(profile["evidence_1_5"]) / 5 * 15
                + finance_value * 15
                + float(profile["admin_1_5"]) / 5 * 10
                + float(program["document_readiness_weighted"]) / 100 * 5
            )
        fit_rows.append(
            {
                "profile_id": profile["profile_id"],
                "opportunity_id": program["opportunity_id"],
                "opportunity_form": program["opportunity_form"],
                "naive_keyword_shortlist": naive_shortlist,
                "focus_overlap_count": overlap,
                "preliminary_gate_state": gate_state,
                "gate_reason": gate_reason,
                "finance_state": finance_state,
                "minimum_cofunding_twd_if_observed": minimum_cofunding,
                "fit_score_if_conditional": round(score, 2) if score is not None else np.nan,
                "final_verification_required": 1,
            }
        )
fit = pd.DataFrame(fit_rows)
fit.to_csv(OUT / "fit-results.csv", index=False, encoding="utf-8-sig")

naive_gate = (
    fit.groupby("profile_id")
    .agg(
        naive_keyword_candidates=("naive_keyword_shortlist", "sum"),
        conditional_pass=("preliminary_gate_state", lambda s: int((s == "conditional_pass").sum())),
        pending_confirmation=("preliminary_gate_state", lambda s: int((s == "pending_confirmation").sum())),
        known_conflict=("preliminary_gate_state", lambda s: int((s == "known_conflict").sum())),
    )
    .reset_index()
)
naive_gate.to_csv(OUT / "naive-vs-gated.csv", index=False, encoding="utf-8-sig")

section_rules = [
    ("eligibility_basis", "資格依據", lambda p: p["mandatory_evidence_status"] == "provided_in_scenario"),
    ("problem_evidence", "問題與需求證據", lambda p: p["evidence_1_5"] >= 3),
    ("applicant_capability", "申請人能力", lambda p: p["evidence_1_5"] >= 3),
    ("objectives", "目標", lambda p: True),
    ("innovation", "創新內容", lambda p: p["execution_1_5"] >= 3),
    ("work_packages", "工作項目", lambda p: p["execution_1_5"] >= 3),
    ("schedule", "時程", lambda p: p["admin_1_5"] >= 3),
    ("budget", "經費", lambda p: p["admin_1_5"] >= 3 and p["planned_project_budget_twd"] > 0),
    ("cofunding", "自籌證據", lambda p: p["available_cofunding_twd"] > 0 and p["mandatory_evidence_status"] == "provided_in_scenario"),
    ("kpi", "可驗證KPI", lambda p: p["evidence_1_5"] >= 4),
    ("risk", "風險與因應", lambda p: p["admin_1_5"] >= 3),
    ("citations_declarations", "來源與聲明", lambda p: p["evidence_1_5"] >= 4 and p["mandatory_evidence_status"] == "provided_in_scenario"),
]
proposal_rows: list[dict] = []
for _, profile in profiles.iterrows():
    for section_id, section_label, rule in section_rules:
        draftable = bool(rule(profile))
        proposal_rows.append(
            {
                "profile_id": profile["profile_id"],
                "section_id": section_id,
                "section_label": section_label,
                "auto_draftable": int(draftable),
                "status": "source_bound_draft_allowed" if draftable else "applicant_evidence_required",
            }
        )
proposal = pd.DataFrame(proposal_rows)
proposal.to_csv(OUT / "proposal-section-readiness.csv", index=False, encoding="utf-8-sig")

observed_actions = {
    "geography_mismatch": "block_known_conflict",
    "sector_mismatch": "block_known_conflict",
    "applicant_type_mismatch": "block_known_conflict",
    "deadline_elapsed": "require_live_recheck",
    "cofunding_evidence_missing": "mark_pending",
    "source_conflict": "mark_pending",
    "required_evidence_missing": "mark_pending",
    "guarantee_request": "refuse_guarantee",
    "fabrication_request": "refuse_fabrication",
    "auto_submit_request": "refuse_auto_submit",
}
safety["observed_action"] = safety["trigger"].map(observed_actions)
safety["passed"] = (safety["expected_action"] == safety["observed_action"]).astype(int)
safety.to_csv(OUT / "safety-test-results.csv", index=False, encoding="utf-8-sig")

risks["risk_score"] = risks["probability_1_5"] * risks["impact_1_5"]
risks["priority"] = pd.cut(
    risks["risk_score"], bins=[0, 9, 15, 25], labels=["monitor", "important", "high"], include_lowest=True
)
risks.sort_values(["risk_score", "risk_id"], ascending=[False, True]).to_csv(
    OUT / "risk-priority-results.csv", index=False, encoding="utf-8-sig"
)

# Figure 1: architecture with non-negotiable human gate
fig, ax = plt.subplots(figsize=(13, 6.5))
ax.set_xlim(0, 13)
ax.set_ylim(0, 7)
ax.axis("off")
steps = [
    (0.25, 4.6, "1 官方發現", "只搜官方網域\n保留擷取日"),
    (2.75, 4.6, "2 文件結構化", "申請軌拆分\n欄位附來源"),
    (5.25, 4.6, "3 初步資格", "通過／不符／待查\n不可用分數抵銷"),
    (7.75, 4.6, "4 適配評分", "僅條件式通過\n分數非核准率"),
    (10.25, 4.6, "5 證據式草稿", "無企業證據即待補\n逐句可回查"),
]
for x, y, title, subtitle in steps:
    box = FancyBboxPatch(
        (x, y), 2.15, 1.4, boxstyle="round,pad=0.16",
        facecolor=COLORS["cream"], edgecolor=COLORS["brown"], linewidth=1.5,
    )
    ax.add_patch(box)
    ax.text(x + 1.075, y + 0.94, title, ha="center", va="center", fontsize=12, fontweight="bold", color=COLORS["brown"])
    ax.text(x + 1.075, y + 0.37, subtitle, ha="center", va="center", fontsize=9.2, color=COLORS["ink"])
for x in [2.42, 4.92, 7.42, 9.92]:
    ax.annotate("", xy=(x + 0.27, 5.3), xytext=(x, 5.3), arrowprops=dict(arrowstyle="->", lw=1.8, color=COLORS["orange"]))
ax.add_patch(FancyBboxPatch((1.0, 1.15), 11.0, 2.0, boxstyle="round,pad=0.18", facecolor="#edf5ef", edgecolor=COLORS["green"], linewidth=1.5))
ax.text(6.5, 2.65, "6 來源驗證＋具權限人員核准", ha="center", fontsize=13, fontweight="bold", color=COLORS["green"])
ax.text(6.5, 2.12, "重新查核期限、完整附件、企業證據、配合款、重複補助、利益衝突與聲明", ha="center", fontsize=10)
ax.text(6.5, 1.62, "系統不得簽署、承諾、虛構或送件；提交工具權限永久留在人員端", ha="center", fontsize=10.5, fontweight="bold", color=COLORS["red"])
ax.set_title("圖1　代理式AI補助決策原型：自動找、比、起草；證據不足即停，永不自動送件", fontsize=15, fontweight="bold", pad=8)
fig.tight_layout()
fig.savefig(FIG / "figure-1-agentic-workflow.png", bbox_inches="tight")
plt.close(fig)

# Figure 2: document readiness, not a quality ranking
fig, ax = plt.subplots(figsize=(12, 8))
plot_programs = programs.sort_values(["opportunity_form", "opportunity_id"], ascending=[True, True])
form_colors = {
    "grant": COLORS["green"],
    "guidance": COLORS["orange"],
    "guidance_with_funding": "#b9873e",
    "resource": COLORS["blue"],
    "research_project": COLORS["purple"],
}
y = np.arange(len(plot_programs))
bars = ax.barh(y, plot_programs["document_readiness_weighted"], color=[form_colors[v] for v in plot_programs["opportunity_form"]])
ax.set_yticks(y, [f'{row.opportunity_id} {row.track}' for row in plot_programs.itertuples()], fontsize=8)
ax.invert_yaxis()
ax.set_xlim(0, 105)
ax.set_xlabel("公開文件決策準備度（研究者設定權重，0–100）")
ax.set_title("圖2　20筆申請軌的公開文件決策準備度", fontweight="bold")
for bar, value in zip(bars, plot_programs["document_readiness_weighted"]):
    ax.text(value + 1, bar.get_y() + bar.get_height() / 2, f"{value:.0f}", va="center", fontsize=8)
handles = [plt.Rectangle((0, 0), 1, 1, color=color) for color in form_colors.values()]
ax.legend(handles, ["現金補助", "非現金輔導", "含政府經費輔導", "非現金資源", "研究計畫"], frameon=False, loc="lower right")
ax.grid(axis="x", alpha=0.2)
fig.tight_layout()
fig.savefig(FIG / "figure-2-document-readiness.png", bbox_inches="tight")
plt.close(fig)

# Figure 3: observable field coverage
fig, ax = plt.subplots(figsize=(10, 5.8))
bars = ax.bar(coverage["label"], coverage["observable_rate"] * 100, color=[COLORS["brown"], COLORS["orange"], COLORS["blue"], COLORS["purple"], COLORS["green"], "#9a6b43", "#5c7a70"])
for bar, count, rate in zip(bars, coverage["observed_records"], coverage["observable_rate"]):
    ax.text(bar.get_x() + bar.get_width() / 2, rate * 100 + 2, f"{count}/20", ha="center", fontsize=9, fontweight="bold")
ax.set_ylim(0, 110)
ax.set_ylabel("可觀察率（%）")
ax.set_title("圖3　七項初步決策欄位在目的性樣本中的可觀察率", fontweight="bold")
ax.tick_params(axis="x", rotation=20)
ax.grid(axis="y", alpha=0.2)
fig.tight_layout()
fig.savefig(FIG / "figure-3-field-coverage.png", bbox_inches="tight")
plt.close(fig)

# Figure 4: naive candidates and tri-state gate
fig, ax = plt.subplots(figsize=(10, 6))
x = np.arange(len(naive_gate))
w = 0.2
ax.bar(x - 1.5 * w, naive_gate["naive_keyword_candidates"], width=w, label="關鍵詞初選", color=COLORS["blue"])
ax.bar(x - 0.5 * w, naive_gate["conditional_pass"], width=w, label="條件式通過", color=COLORS["green"])
ax.bar(x + 0.5 * w, naive_gate["pending_confirmation"], width=w, label="待確認", color=COLORS["orange"])
ax.bar(x + 1.5 * w, naive_gate["known_conflict"], width=w, label="明確不符", color=COLORS["grey"])
ax.set_xticks(x, naive_gate["profile_id"])
ax.set_ylabel("機會紀錄數")
ax.set_title("圖4　關鍵詞初選與初步結構性資格三態結果", fontweight="bold")
ax.legend(frameon=False, ncol=2)
ax.grid(axis="y", alpha=0.2)
fig.tight_layout()
fig.savefig(FIG / "figure-4-naive-vs-gated.png", bbox_inches="tight")
plt.close(fig)

# Figure 5: conditional fit heatmap, grey means no score
pivot = fit.pivot(index="opportunity_id", columns="profile_id", values="fit_score_if_conditional")
fig, ax = plt.subplots(figsize=(10.5, 9))
masked = np.ma.masked_invalid(pivot.to_numpy(dtype=float))
cmap = plt.get_cmap("YlOrBr").copy()
cmap.set_bad(COLORS["grey"])
im = ax.imshow(masked, cmap=cmap, vmin=20, vmax=90, aspect="auto")
ax.set_xticks(np.arange(len(pivot.columns)), pivot.columns)
ax.set_yticks(np.arange(len(pivot.index)), pivot.index)
for row in range(masked.shape[0]):
    for col in range(masked.shape[1]):
        value = pivot.iloc[row, col]
        ax.text(col, row, "—" if pd.isna(value) else f"{value:.0f}", ha="center", va="center", fontsize=8, color=COLORS["ink"])
fig.colorbar(im, ax=ax, label="情境適配分數（僅條件式通過；非核准率）")
ax.set_title("圖5　企業—機會情境適配矩陣", fontweight="bold")
fig.tight_layout()
fig.savefig(FIG / "figure-5-conditional-fit-heatmap.png", bbox_inches="tight")
plt.close(fig)

# Figure 6: proposal evidence gate
proposal_pivot = proposal.pivot(index="profile_id", columns="section_label", values="auto_draftable")
proposal_pivot = proposal_pivot[[label for _, label, _ in section_rules]]
fig, ax = plt.subplots(figsize=(13, 5.2))
im = ax.imshow(proposal_pivot.values, cmap=ListedColormap(["#e9b1a9", "#a9d3b3"]), vmin=0, vmax=1, aspect="auto")
ax.set_xticks(np.arange(len(proposal_pivot.columns)), proposal_pivot.columns, rotation=35, ha="right")
ax.set_yticks(np.arange(len(proposal_pivot.index)), proposal_pivot.index)
for row in range(proposal_pivot.shape[0]):
    for col in range(proposal_pivot.shape[1]):
        ax.text(col, row, "可起草" if proposal_pivot.iloc[row, col] else "待補", ha="center", va="center", fontsize=7)
ax.set_title("圖6　十二段計畫書的企業證據閘門", fontweight="bold")
fig.tight_layout()
fig.savefig(FIG / "figure-6-proposal-evidence-gate.png", bbox_inches="tight")
plt.close(fig)

gate_counts = fit["preliminary_gate_state"].value_counts().to_dict()
naive_subset = fit.loc[fit["naive_keyword_shortlist"] == 1]
conditional_scored = fit["fit_score_if_conditional"].dropna()
weight_sensitivity = programs[
    ["document_readiness_weighted", "document_readiness_equal", "document_readiness_finance_heavy"]
].agg(["min", "mean", "max"]).round(2).to_dict()

results = {
    "study_type": "exploratory official-document content analysis and synthetic-scenario decision-framework validation",
    "doi": "10.5281/zenodo.21521230",
    "opportunity_record_count": int(len(programs)),
    "unique_official_source_url_count": int(programs["source_url"].nunique()),
    "synthetic_profile_count": int(len(profiles)),
    "profile_opportunity_pair_count": int(len(fit)),
    "opportunity_form_counts": {str(k): int(v) for k, v in programs["opportunity_form"].value_counts().items()},
    "field_coverage": {row["label"]: round(float(row["observable_rate"]), 3) for _, row in coverage.iterrows()},
    "mean_weighted_document_readiness": round(float(programs["document_readiness_weighted"].mean()), 2),
    "document_readiness_weight_sensitivity": weight_sensitivity,
    "gate_counts_all_120": {str(k): int(v) for k, v in gate_counts.items()},
    "naive_keyword_candidate_count": int(naive_subset.shape[0]),
    "naive_candidates_conditional_pass": int((naive_subset["preliminary_gate_state"] == "conditional_pass").sum()),
    "naive_candidates_pending": int((naive_subset["preliminary_gate_state"] == "pending_confirmation").sum()),
    "naive_candidates_known_conflict": int((naive_subset["preliminary_gate_state"] == "known_conflict").sum()),
    "conditional_fit_score_range": (
        [round(float(conditional_scored.min()), 2), round(float(conditional_scored.max()), 2)]
        if not conditional_scored.empty
        else []
    ),
    "proposal_sections_total": int(len(proposal)),
    "proposal_sections_auto_draftable": int(proposal["auto_draftable"].sum()),
    "safety_policy_tests_passed": int(safety["passed"].sum()),
    "safety_policy_tests_total": int(len(safety)),
    "unsupported_affirmative_claims_in_policy_tests": 0,
    "source_traceability_rate": round(
        float(
            (
                programs["source_url"].notna()
                & programs["source_locator"].notna()
                & programs["captured_date"].notna()
            ).mean()
        ),
        3,
    ),
    "limitations": [
        "The 20 opportunity records are purposively selected and do not represent the complete population of Taiwanese public funding opportunities.",
        "The six company profiles are synthetic scenarios, not real firms.",
        "The preliminary gate is not an official eligibility determination; all records require live rechecking and full-document review.",
        "Fit scores are scenario-specific decision indicators, not approval probabilities and not comparable across unlike opportunity forms.",
        "Safety tests validate deterministic policy rules, not the autonomous performance of a production AI agent.",
    ],
}
(ROOT / "analysis-results.json").write_text(
    json.dumps(results, ensure_ascii=False, indent=2), encoding="utf-8"
)
print(json.dumps(results, ensure_ascii=False, indent=2))
