#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Generate weekly_opportunity_YYYYMMDD.html from verified projects.

Inputs : projects_20260917.json (idx = position+1), screenshots/20260917/pNN.png,
         snapshots/20260917/sNN.txt
Rules  : 铁律0 — a project with no valid screenshot (>=100KB) OR no text snapshot
         is DROPPED entirely. Scoring follows the business-intelligence skill:
         field weight + region bonus + keyword bonus; 契合点 by explicit patterns.
"""
import io, json, os, re, sys, datetime

DATE = sys.argv[1] if len(sys.argv) > 1 else "20260917"
BASE = "/data/www/files"
SHOT = os.path.join(BASE, "screenshots", DATE)
SNAP = os.path.join(BASE, "snapshots", DATE)
TODAY = datetime.date(2026, 9, 17)
CYCLE_START, CYCLE_END = "2026-08-17", "2026-09-17"

projs = json.load(io.open(os.path.join(BASE, "projects_20260917.json"), encoding="utf-8"))

# ---------- helpers ----------
def snip_text(i):
    p = os.path.join(SNAP, "s%02d.txt" % i)
    if not os.path.exists(p):
        return ""
    return io.open(p, encoding="utf-8", errors="ignore").read()

def shot_ok(i):
    p = os.path.join(SHOT, "p%02d.png" % i)
    return os.path.exists(p) and os.path.getsize(p) >= 100000

def parse_date(s):
    m = re.search(r"(20\d{2})\s*[-年/.]\s*(\d{1,2})\s*[-月/.]\s*(\d{1,2})", s)
    if not m:
        return None
    try:
        return datetime.date(int(m.group(1)), int(m.group(2)), int(m.group(3)))
    except ValueError:
        return None

def extract(text):
    """Pull deadline + budget out of the announcement body (real page text only)."""
    dl, bud = None, None
    for pat in [r"(?:投标|响应文件|响应|申请|报价|递交)[^。\n]{0,12}截止(?:时间|日期)?[：:]\s*([^\n。；;]{0,40})",
                r"截止(?:时间|日期)[：:]\s*([^\n。；;]{0,40})",
                r"(?:开标|响应文件开启)(?:时间|日期)?[：:]\s*([^\n。；;]{0,40})"]:
        m = re.search(pat, text)
        if m:
            d = parse_date(m.group(1))
            if d:
                dl = d
                break
    m = re.search(r"预算(?:总)?金额[^0-9]{0,12}([\d,]+(?:\.\d+)?)\s*万元", text)
    if not m:
        m = re.search(r"预算(?:总)?金额[^0-9]{0,12}([\d,]+(?:\.\d+)?)\s*元", text)
        if m:
            bud = round(float(m.group(1).replace(",", "")) / 10000, 2)
    if m and bud is None:
        bud = float(m.group(1).replace(",", ""))
    if bud is None:
        m = re.search(r"中标(?:（成交）|\(成交\))?(?:金额|价)[^0-9]{0,12}([\d,]+(?:\.\d+)?)\s*万元", text)
        if m:
            bud = float(m.group(1).replace(",", ""))
    return dl, bud

# ---------- scoring ----------
FIT_ORDER = [
    (lambda t, b: ("党校" in t or "干部学院" in t) and any(k in t for k in ("信息化", "智慧", "平台", "系统")),
     "我司有党校信息化成熟产品，可直接复用"),
    (lambda t, b: ("党校" in t or "干部学院" in t or "行政学院" in t) and any(k in t for k in ("教学", "录播", "媒体", "研讨", "课程", "课件", "培训")),
     "党校教学/媒体制作设施与我司录播、课件制作业务高度契合"),
    (lambda t, b: "组织部" in t and any(k in t for k in ("党建", "系统", "平台")),
     "我司已有党建平台建设案例，经验丰富"),
    (lambda t, b: "组织部" in t and "培训" in t, "我司有党员干部培训内容与平台服务能力"),
    (lambda t, b: "党员教育" in t and any(k in t for k in ("课件", "视频", "课程", "培训", "平台", "系统", "网络")),
     "我司具备党员教育内容制作与平台建设能力"),
    (lambda t, b: any(k in t for k in ("展陈", "展览", "布展", "陈列")) and any(k in t for k in ("红色", "党建", "廉政", "廉洁", "革命", "烈士", "纪念馆", "文史")),
     "我司有红色展陈/宣传片制作经验"),
    (lambda t, b: any(k in t for k in ("展陈", "展览", "布展")), "展陈内容制作与我司内容业务相关，可关注合作"),
    (lambda t, b: "融媒体" in t or "融媒体中心" in b, "融媒体内容制作与传播是我司可承接业务"),
    (lambda t, b: any(k in t for k in ("宣传片", "专题片", "纪录片", "宣传视频", "拍摄", "摄制", "制片")),
     "宣传视频制作与我司内容制作主业相关，可关注合作"),
    (lambda t, b: "思政" in t and any(k in t for k in ("国企", "数字化")), "国企思政数字化是我司重点拓展方向"),
    (lambda t, b: "智慧党建" in t or ("党建" in t and any(k in t for k in ("平台", "数智", "数字化", "信息化", "智慧"))),
     "与智慧党建平台业务高度契合，建议重点跟进"),
    (lambda t, b: any(k in t for k in ("系统建设", "运维", "平台建设")), "我司有系统建设与运维全流程服务能力"),
    (lambda t, b: "培训" in t or "能力提升" in t, "培训服务与内容开发是我司核心业务方向"),
    (lambda t, b: "智慧校园" in t or "教育信息化" in t, "教育信息化是我司可承接方向"),
]

CORE = ("党校", "干部学院", "行政学院", "干部教育", "党员教育", "党建", "组织部", "党群")

def field_weight(t, b):
    if any(k in t for k in ("智慧党建", "智慧党校")) or any(k in t for k in ("党校", "干部学院", "干部教育培训", "干部教育", "干部培训", "行政学院")):
        return 50
    if "党员教育" in t and any(k in t for k in ("平台", "系统", "网络", "综合")):
        return 50
    if "党建" in t and any(k in t for k in ("平台", "系统", "数智", "数字化", "信息化", "智慧", "应用")):
        return 50
    if any(k in t for k in ("融媒体", "信息化", "政务", "智慧校园", "数字化")) or "融媒体中心" in b:
        return 40
    if "组织部" in t and any(k in t for k in ("平台", "系统")):
        return 40
    if any(k in t for k in ("展陈", "展览", "布展", "改陈布展", "体验馆")):
        return 40
    if any(k in t for k in ("党群服务中心", "党建阵地", "党群服务站", "阵地建设")):
        return 40
    if "党建" in t:
        return 40
    if any(k in t for k in ("培训", "能力提升", "职业教育", "高等教育", "教育质量", "教育提升", "开放大学")):
        return 40
    if any(k in t for k in ("宣传片", "专题片", "纪录片", "微短剧", "视频拍摄", "视频制作", "拍摄", "微课", "课程", "摄制", "宣传视频")):
        return 30
    return 10

KW = ["智慧", "云平台", "数字化", "信息化", "融媒体", "网络培训", "培训", "能力提升",
      "视频拍摄", "党员教育片", "课程制作", "直播", "视频", "课件", "系统建设", "运维"]
# HARD noise: goods/services entirely outside our business — cap regardless of buyer
HARD_NOISE = ["家具", "空调", "餐饮", "食堂", "物业", "书籍", "图书", "桌椅", "易耗品", "宿舍", "后勤", "食材"]
# SOFT noise: "设备采购" is only noise when nothing teaching/media/digital is in the title
SOFT_NOISE = ["设备采购"]
TEACH = ["教学", "培训", "教育", "录播", "媒体", "信息", "智慧", "系统", "平台", "课件", "课程", "展陈"]

def score(t, b, prov):
    s = field_weight(t, b)
    t2 = t.replace("卫视频道", "")
    for k in KW:
        if k in t2:
            s += 5
    if prov in ("四川", "青海", "重庆", "云南", "贵州"):
        s += 20
    if any(n in t for n in HARD_NOISE):
        s = min(s, 30)
    elif any(n in t for n in SOFT_NOISE) and not any(k in t for k in TEACH):
        s = min(s, 30)
    return s

def fit_text(t, b, m):
    for fn, txt in FIT_ORDER:
        try:
            if fn(t, b):
                return txt
        except Exception:
            pass
    if m >= 80:
        return "与我司核心业务高度契合"
    if m >= 60:
        return "与我司业务有一定关联，可探索合作"
    return "业务关联度一般，可关注"

def make_note(t, ty, s):
    core = []
    for k in ("智慧党建", "党校", "组织部", "党员教育", "融媒体", "展陈", "宣传片", "信息化", "智慧校园", "培训", "平台"):
        if k in t:
            core.append(k)
    core = core[:3]
    return ("命中：" + "/".join(core) if core else "行业拓展") + "｜" + ty

# ---------- build ----------
kept, dropped = [], []
for i, p in enumerate(projs, 1):
    txt = snip_text(i)
    ok_shot, ok_snap = shot_ok(i), len(txt.strip()) > 80
    if not (ok_shot and ok_snap):
        dropped.append((i, p["title"], "shot=%s snap=%s" % (ok_shot, ok_snap)))
        continue
    dl, bud = extract(txt)
    ty = p["type"]
    budget = p["budget"]
    if bud is not None:
        budget = bud
    if budget == "未披露":
        budget_v = "未披露"
    else:
        try:
            budget_v = round(float(str(budget).replace(",", "")), 2)
        except Exception:
            budget_v = "未披露"
    if ty == "中标公告":
        status = "已中标"
    elif ty in ("意向公开",):
        status = "意向预告"
    elif ty in ("单一来源公示",):
        status = "意向预告"
    else:
        status = "可参与"
        if dl and dl < TODAY:
            status = "已截止"
    if status == "已截止":
        dropped.append((i, p["title"], "截止日期 %s 已过期" % dl))
        continue
    m = score(p["title"], p["buyer"], p["province"])
    lvl = "高" if m >= 70 else ("中" if m >= 40 else "低")
    kept.append({
        "idx": 0, "title": p["title"], "buyer": p["buyer"], "province": p["province"],
        "budget": budget_v, "deadline": dl.isoformat() if dl else "未披露",
        "pub_date": p["pub_date"], "info_type": ty, "source": p["source"], "url": p["url"],
        "note": make_note(p["title"], ty, m), "fit": fit_text(p["title"], p["buyer"], m),
        "match": m, "match_level": lvl, "status": status,
        "screenshot": "screenshots/%s/p%02d.png" % (DATE, i),
        "_src_idx": i,
    })
for n, p in enumerate(kept, 1):
    p["idx"] = n

# orphan cleanup so 截图数 == 项目数 exactly
used = set(p["_src_idx"] for p in kept)
orphans = []
for i in range(1, len(projs) + 1):
    if i in used:
        continue
    for f in (os.path.join(SHOT, "p%02d.png" % i), os.path.join(SNAP, "s%02d.txt" % i)):
        if os.path.exists(f):
            os.remove(f)
            orphans.append(os.path.basename(f))

ns = len(kept)
n_shot = sum(1 for p in kept if os.path.exists(os.path.join(BASE, p["screenshot"])))
n_snap = sum(1 for p in kept if len(snip_text(p["_src_idx"]).strip()) > 80)

print("kept=%d dropped=%d" % (len(kept), len(dropped)))
for d in dropped:
    print("  DROP #%d %s | %s" % d)
print("orphans removed:", orphans)
print("shot=%d snap=%d" % (n_shot, n_snap))

# ---------- inject ----------
html = io.open(os.path.join(BASE, "report_template.html"), encoding="utf-8").read()
rep = {
    "__PROJECTS_JSON__": json.dumps(kept, ensure_ascii=False),
    "__TITLE__": "2026年9月 第3周 商业机会资讯",
    "__CYCLE_START__": CYCLE_START, "__CYCLE_END__": CYCLE_END,
    "__GEN_DATE__": TODAY.isoformat(), "__DATE_DIR__": DATE,
    "__V_RATE__": "100%% (%d/%d)" % (ns, ns),
    "__V_SHOTS__": "%d/%d条" % (n_shot, ns),
    "__V_SNAPS__": "%d/%d条" % (n_snap, ns),
}
for k, v in rep.items():
    html = html.replace(k, v)
out = os.path.join(BASE, "weekly_opportunity_%s.html" % DATE)
io.open(out, "w", encoding="utf-8").write(html)
json.dump(kept, io.open(os.path.join(BASE, "final_projects_%s.json" % DATE), "w", encoding="utf-8"),
          ensure_ascii=False, indent=1)
print("written:", out, len(html), "bytes")
print("high=%d mid=%d low=%d" % (sum(1 for p in kept if p["match_level"] == "高"),
                                sum(1 for p in kept if p["match_level"] == "中"),
                                sum(1 for p in kept if p["match_level"] == "低")))
