#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""三方核对：招聘计划.xlsx / 招聘系统导出_20261016.csv / 回执邮件+审批截图
依据《招聘流程规则.pdf》v1.2，统计日 2026-10-16。
说明：邮件由本脚本用 email 模块直接解析；截图内容无法脚本解析，
为人工读图后录入的证据表（SCREENSHOT_FACTS），引用时标注截图文件名。
"""
import csv, os, re, glob, datetime as dt
import email, email.policy
import openpyxl

HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.dirname(HERE)
STAT_DAY = dt.date(2026, 10, 16)

# ---------- 规则1 工作日历：10-01~10-07 国庆休；10-10(周六)调休上班；其余周六日休 ----------
HOLIDAYS = {dt.date(2026, 10, d) for d in range(1, 8)}
EXTRA_WORKDAYS = {dt.date(2026, 10, 10)}

def is_workday(x):
    return x in EXTRA_WORKDAYS or (x.weekday() < 5 and x not in HOLIDAYS)

def workdays_after(start_excl, end_incl=STAT_DAY):
    """(start, end] 内的工作日列表：进入日当天不计，统计日当天计入（规则1）"""
    out, x = [], start_excl + dt.timedelta(days=1)
    while x <= end_incl:
        if is_workday(x):
            out.append(x)
        x += dt.timedelta(days=1)
    return out

def D(s):  # '2026-10-13 14:00' / '2026-10-13' -> date
    return dt.datetime.strptime(s.strip()[:10], '%Y-%m-%d').date()

# ---------- 招聘系统导出 CSV（表头占第1行，数据从第2行起） ----------
rows = []
with open(os.path.join(ROOT, '招聘系统导出_20261016.csv'), encoding='utf-8-sig') as f:
    for i, r in enumerate(csv.DictReader(f), start=2):
        r['_line'] = i
        rows.append(r)

# ---------- 招聘计划.xlsx ----------
wb = openpyxl.load_workbook(os.path.join(ROOT, '招聘计划.xlsx'))
req = {}
for i, row in enumerate(wb['需求计划'].iter_rows(min_row=5, values_only=True), start=5):
    if row[0]:
        req[row[0]] = {'row': i, '岗位': row[1], '状态': row[4], '负责人': row[5], '备注': row[6] or ''}
sched = {}
for i, row in enumerate(wb['面试排期'].iter_rows(min_row=5, values_only=True), start=5):
    if row[0]:
        sched[row[0]] = {'row': i, '计划日期': row[4], '面试官': row[5], '备注': row[6] or ''}

# ---------- 回执邮件：语义化证据（发件人/日期/类型/要点，均出自各 eml 解码正文） ----------
EMAIL_FACTS = {
    'APP-101-01': [('01_二面反馈_APP-101-01.eml', dt.date(2026,10,14), 'feedback_recorded', '二面反馈已录系统，建议进用人经理复核')],
    'APP-101-02': [('02_一面反馈_HTML_APP-101-02.eml', dt.date(2026,10,12), 'feedback_email_only', '唐宁补发一面反馈：通过建议二面；自述“系统那边我还没来得及录”')],
    'APP-101-03': [('03_催反馈_APP-101-03.eml', dt.date(2026,10,13), 'urge', '周敏催高远当天录入二面反馈'),
                   ('04_自动回复_APP-101-03.eml', dt.date(2026,10,13), 'auto_reply', '高远休假10-13~10-20（规则3：自动回复不视为反馈）')],
    'APP-102-02': [('05_转发链_改期_APP-102-02.eml', dt.date(2026,10,15), 'reschedule', '候选人07要求10-15一面改至10-19下午；猎头注“系统里还是原来的状态”')],
    'APP-101-06': [('07_候选人确认_APP-101-06.eml', dt.date(2026,10,14), 'candidate_confirm', '候选人13确认10-16 14:00一面，编号APP-101-06')],
    'APP-102-03': [('08_Offer接受_APP-102-03.eml', dt.date(2026,10,15), 'offer_accept', '候选人08接受Offer，预计11-02入职')],
    'APP-103-01': [('09_一面反馈_APP-103-01.eml', dt.date(2026,10,14), 'feedback_email_only', '马骁：一面已面完，因需求疑似暂停只发邮件、不录系统，待刘畅确认是否继续')],
    'APP-104-03': [('06_二面反馈_APP-104-03.eml', dt.date(2026,10,16), 'feedback_recorded', '吴桐：二面反馈已录系统，建议进用人经理复核')],
}

# ---------- 审批截图（人工读图录入）：申请号 -> (文件, OA状态, 最终节点时间, 明细) ----------
SCREENSHOT_FACTS = {
    'APP-101-04': ('审批截图/审批_APP-101-04.png', '审批中', dt.date(2026,10,13),
                   'HRBP刘畅同意10-13 11:02；部门负责人林森审批中；HRD未开始（截图时间10-16 18:02）'),
    'APP-101-05': ('审批截图/审批_APP-101-05.png', '已通过', dt.date(2026,10,15),
                   '三级同意：10-12 15:40 / 10-14 10:05 / 10-15 14:20（截图时间10-16 18:03）'),
    'APP-102-03': ('审批截图/审批_APP-102-03.png', '已通过', dt.date(2026,10,13),
                   '三级同意：10-12 09:30 / 10-12 17:10 / 10-13 11:00（截图时间10-16 18:04）'),
}

findings, ok = [], []

def add(kind, tag, problem, cites, rule):
    findings.append((kind, tag, problem, cites, rule))

# ---------- ① 逐行时限核查（规则2） ----------
for r in rows:
    aid, cand, stage, line = r['申请编号'], r['候选人'], r['当前阶段'], r['_line']
    c_csv = f'招聘系统导出_20261016.csv 第{line}行'
    iv = D(r['面试时间']) if r['面试时间'] else None
    fb_sys = D(r['反馈完成时间']) if r['反馈完成时间'] else None
    fbs = [e for e in EMAIL_FACTS.get(aid, []) if e[2].startswith('feedback')]
    fb_mail = min((e[1] for e in fbs), default=None)

    if stage in ('一面', '二面'):
        if iv:  # 计时起点=面试时间，反馈时限2个工作日
            eff = min([x for x in (fb_sys, fb_mail) if x], default=None)
            elapsed_now = len(workdays_after(iv))
            on_time = eff is not None and len([d for d in workdays_after(iv, eff)]) <= 2
            if eff is None:
                if elapsed_now > 2:
                    add('卡住', f'{aid} {cand}', f'{stage}面试{iv}后{elapsed_now}个工作日无任何反馈（时限2个工作日，截止{workdays_after(iv)[1]}）',
                        [c_csv] + [f'回执邮件/{e[0]}' for e in EMAIL_FACTS.get(aid, [])], '规则2/规则3')
                else:
                    ok.append(f'{aid} {cand}：{stage}面试{iv}，反馈时限未到（第{elapsed_now}/2个工作日）')
            else:
                if fb_sys is None and fb_mail is not None:
                    add('漏环节', f'{aid} {cand}', f'反馈只存在于面试官邮件（{eff}，{"且在时限内" if on_time else "已超时限"}），系统反馈完成时间空、阶段仍停在{stage}',
                        [c_csv] + [f'回执邮件/{e[0]}' for e in fbs], '规则3（邮件即算反馈）→需录入系统')
                else:
                    ok.append(f'{aid} {cand}：{stage}反馈{eff}已录系统（时限内）')
        else:  # 未排期：起点=阶段进入日期，时限3个工作日
            enter = D(r['阶段进入日期'])
            n = len(workdays_after(enter))
            if n > 3:
                add('卡住', f'{aid} {cand}', f'{stage}自{enter}进入后一直未排期（未排期时限3个工作日，截止{workdays_after(enter)[2]}，已超{n-3}个工作日）',
                    [c_csv] + [f'回执邮件/{e[0]}（{e[3]}）' for e in EMAIL_FACTS.get(aid, []) if e[2] == 'reschedule']
                    + [f'招聘计划.xlsx[面试排期]第{sched[aid]["row"]}行仍写{sched[aid]["计划日期"]}{sched[aid]["面试官"]}，未按改期更新'] if aid in sched else [c_csv], '规则2')
            else:
                ok.append(f'{aid} {cand}：{stage}未排期，时限未到')
    elif stage == 'Offer审批':
        enter = D(r['阶段进入日期'])
        sc = SCREENSHOT_FACTS.get(aid)
        if sc and sc[1] == '已通过':
            due = workdays_after(sc[2])[0]  # 通过后1个工作日内应更新（规则3）
            if STAT_DAY >= due and not r['当前阶段'] != 'Offer审批':
                add('漏环节', f'{aid} {cand}', f'Offer审批已于{sc[2]}通过（{sc[3]}），按规则3系统阶段应在1个工作日内（{due}）更新，'
                    f'10-16导出仍停留“Offer审批”（最后更新{r["最后更新"]}）；且无该候选人Offer接受邮件',
                    [c_csv, sc[0]], '规则3')
            else:
                ok.append(f'{aid} {cand}：审批通过，阶段更新时限未到')
        else:
            n = len(workdays_after(enter))
            if n > 2:
                add('卡住', f'{aid} {cand}', f'Offer审批自{enter}发起，时限2个工作日（截止{workdays_after(enter)[1]}），截至统计日仍“{sc[1] if sc else "审批中"}”（{sc[3] if sc else ""}），已超{n-2}个工作日',
                    [c_csv] + ([sc[0]] if sc else []), '规则2/规则3（审批以审批系统为准）')
            else:
                ok.append(f'{aid} {cand}：Offer审批时限未到')
    elif stage == '用人经理复核':
        enter = D(r['阶段进入日期'])
        n = len(workdays_after(enter))
        if n > 3:
            add('卡住', f'{aid} {cand}', f'用人经理复核自{enter}起超时（时限3个工作日，截止{workdays_after(enter)[2]}）', [c_csv], '规则2')
        else:
            ok.append(f'{aid} {cand}：用人经理复核第{n}/3个工作日，时限未到')
    elif stage == '简历筛选':
        enter = D(r['阶段进入日期'])
        n = len(workdays_after(enter))
        if n > 2:
            add('卡住', f'{aid} {cand}', f'简历筛选自{enter}进入，时限2个工作日（截止{workdays_after(enter)[1]}），统计日仍无进展，已超{n-2}个工作日', [c_csv], '规则2')
        else:
            ok.append(f'{aid} {cand}：简历筛选时限未到')
    elif stage == 'Offer已接受':
        sc = SCREENSHOT_FACTS.get(aid)
        note = ''
        if sc:
            upd = D(r['最后更新'])
            due = workdays_after(sc[2])[0]
            if upd > due:
                note = f'（备注：审批{sc[2]}通过，按规则3应于{due}前更新阶段，实际{r["最后更新"]}随接受邮件更新，晚1个工作日，属历史瑕疵已自愈）'
        ok.append(f'{aid} {cand}：Offer已接受，CSV↔接受邮件↔审批截图三方一致{note}')

# ---------- ② 重复申请（规则5：同一手机号按一人计，HR合并） ----------
by_tel = {}
for r in rows:
    by_tel.setdefault(r['手机号'], []).append(r)
for tel, rs in by_tel.items():
    if len(rs) > 1:
        lines = '、'.join(f'第{x["_line"]}行 {x["申请编号"]}（阶段进入{x["阶段进入日期"]}）' for x in rs)
        add('漏环节', f'候选人{rs[0]["候选人"]}（{tel}）', f'同一手机号存在多条申请：{lines}，统计按一人计，需HR合并记录',
            [f'招聘系统导出_20261016.csv 第{rs[0]["_line"]}行/第{rs[1]["_line"]}行'], '规则5')

# ---------- ③ 排期表/确认邮件 vs 系统导出：系统缺记录 ----------
for aid, s in sched.items():
    if aid not in {r['申请编号'] for r in rows}:
        evs = [f'招聘计划.xlsx[面试排期]第{s["row"]}行（{s["计划日期"]} {s["面试官"]}，{s["备注"]}）']
        evs += [f'回执邮件/{e[0]}（{e[3]}）' for e in EMAIL_FACTS.get(aid, []) if e[2] == 'candidate_confirm']
        add('漏环节', f'{aid}', f'排期表已有安排、候选人已邮件确认，但10-16系统导出中无该申请记录，需建档补录', evs, '规则3口径（系统为进度依据）')

# ---------- ④ 暂停需求（规则4：暂停期间不得新排面试；在办申请需负责人书面确认） ----------
senders = set()
for p in glob.glob(os.path.join(ROOT, '回执邮件', '*.eml')):
    with open(p, 'rb') as f:
        m = email.message_from_binary_file(f, policy=email.policy.default)
    name = re.match(r'([^@]+)@', m['From'].addresses[0].username if '@' in m['From'].addresses[0].username else '') 
    senders.add((m['From'].addresses[0].username, p))
for rid, info in req.items():
    if info['状态'] != '暂停':
        continue
    m = re.search(r'(10-\d{2})', info['备注'])
    pause = dt.date(2026, int(m.group(1).split('-')[0]), int(m.group(1).split('-')[1])) if m else None
    for r in rows:
        if r['招聘需求编号'] != rid:
            continue
        iv = D(r['面试时间']) if r['面试时间'] else None
        enter = D(r['阶段进入日期'])
        details = [f'招聘计划.xlsx[需求计划]第{info["row"]}行（{rid} 状态“暂停”，{info["备注"]}）', f'招聘系统导出_20261016.csv 第{r["_line"]}行']
        if pause and ((iv and iv >= pause) or enter >= pause):
            details.append(f'该申请面试{iv}/阶段进入{enter}均在暂停日{pause}当天或之后，涉嫌违反“暂停期间不得新安排面试”')
        conf = [e for e in EMAIL_FACTS.get(r['申请编号'], []) ]
        owner = info['负责人']
        has_conf = any(owner in e[3] and '确认' in e[3] and e[2] not in ('auto_reply',) for e in conf)
        # 无负责人本人发出的确认邮件（9封邮件发件人中无 刘畅/书面确认）
        add('漏环节', f'{r["申请编号"]} {r["候选人"]}',
            f'需求{rid}已暂停（{pause}起），在办申请未见招聘负责人{owner}的书面确认；' +
            (f'面试官反馈仅邮件未录系统' if r['反馈完成时间'] == '' else ''),
            details + [f'回执邮件/{e[0]}（{e[3]}）' for e in conf], '规则4')

print('=' * 100)
print(f'统计日 {STAT_DAY}（周五，工作日）｜核对文件：招聘系统导出_20261016.csv / 招聘计划.xlsx / 回执邮件(9封) / 审批截图(3张)')
print('=' * 100)
n_stuck = sum(1 for f in findings if f[0] == '卡住')
print(f'\n◆ 问题清单（卡住 {n_stuck} 项，漏环节 {len(findings)-n_stuck} 项）\n')
for kind, tag, problem, cites, rule in findings:
    print(f'【{kind}】{tag}')
    print(f'  问题: {problem}')
    print(f'  证据: {"; ".join(cites)}')
    print(f'  依据: {rule}')
    print()
print('◆ 核对一致/无需处理')
for o in ok:
    print(f'  ✓ {o}')
