"""
Workforce Planning Analytics — Sunny Valley International
=========================================================
Employee register -> HR data-quality rules -> current-state workforce analytics
-> 3-year workforce plan (demand, attrition, hiring, Saudization, payroll index)
-> JSON feed for the bilingual executive dashboard.

Author : Adel Asiri (CDMP, PMP)
Stack  : Python 3 · pandas · NumPy

    python hr_pipeline.py      ->  output/hr_dashboard.json, output/dq_report.csv
Privacy: pseudonymised IDs only; pay is published as shares and a relative index,
never as amounts; cost groups with fewer than 3 employees are merged.
"""
from __future__ import annotations

import json
from datetime import date, datetime, timezone
from pathlib import Path

import numpy as np
import pandas as pd

ROOT = Path(__file__).resolve().parent
DATA, OUT = ROOT / "data", ROOT / "output"
OUT.mkdir(exist_ok=True)
TODAY = date(2026, 9, 27)
YEARS = [2027, 2028, 2029]          # plan horizon: end of academic years 2026-27 … 2028-29
MIN_CELL = 3                        # small-cell suppression for pay analytics

FAMILIES = {
    "TCH": ("التعليم", "Teaching"), "TRN": ("النقل المدرسي", "School transport"),
    "SUP": ("الخدمات المساندة", "Support services"), "ADM": ("الإدارة", "Administration"),
    "FIN": ("المالية", "Finance"), "HRM": ("الموارد البشرية", "Human resources"),
}
NATS = {"SA": ("سعودي", "Saudi"), "EG": ("مصري", "Egyptian"), "IN": ("هندي", "Indian")}
LOCALIZABLE = {"TCH", "TRN"}             # hiring mix follows the Saudi-share policy
SAUDI_ONLY = {"ADM", "FIN", "HRM"}        # already fully Saudi today; replacements stay Saudi
EXPAT_OF = {"TCH": "EG", "TRN": "EG", "SUP": "IN", "ADM": "EG", "FIN": "EG", "HRM": "EG"}
GROWTH_FACTOR = {"TCH": 1.0, "TRN": 1.0, "ADM": 0.5, "FIN": 0.0, "HRM": 0.0, "SUP": 0.0}


def load():
    emp = pd.read_csv(DATA / "employees.csv", encoding="utf-8-sig", keep_default_na=False)
    struct = pd.read_csv(DATA / "workforce_structure.csv")
    a = pd.read_csv(DATA / "planning_assumptions.csv").set_index("parameter")["value"].astype(float).to_dict()
    for c in ["hire_date", "contract_end"]:
        emp[c] = pd.to_datetime(emp[c]).dt.date
    emp["iqama_expiry"] = pd.to_datetime(emp["iqama_expiry"].replace("", None)).dt.date
    return emp, struct, a


# ---------------------------------------------------------------- data quality
def data_quality(emp, struct):
    n = len(emp); rules = []
    def rule(rid, dim_ar, dim_en, ar, en, failed, act_ar, act_en, checked=None):
        checked = checked or n
        rules.append(dict(id=rid, dim_ar=dim_ar, dim_en=dim_en, rule_ar=ar, rule_en=en, checked=int(checked),
                          failed=int(failed), pass_rate=round(100 * (1 - failed / checked), 1), action_ar=act_ar, action_en=act_en))
    expat = emp["nationality"] != "SA"
    rule("HR-01", "التفرد", "Uniqueness", "رقم الموظف المستعار فريد", "Pseudonymised employee ID is unique",
         emp["employee_id"].duplicated().sum(), "دمج السجلات المكررة", "Merge duplicate records")
    rule("HR-02", "الصحة", "Validity", "الجنسية من القائمة المرجعية", "Nationality from reference list",
         (~emp["nationality"].isin(NATS)).sum(), "تصحيح الترميز", "Fix coding")
    rule("HR-03", "الصحة", "Validity", "الأسرة الوظيفية من الدليل المعتمد", "Job family from approved catalogue",
         (~emp["job_family"].isin(FAMILIES)).sum(), "تصحيح الترميز", "Fix coding")
    rule("HR-04", "الاكتمال", "Completeness", "تاريخ انتهاء الإقامة موجود لغير السعوديين", "Iqama expiry present for expatriates",
         (expat & emp["iqama_expiry"].isna()).sum(), "استكمال التاريخ من منصة مقيم", "Complete from Muqeem", checked=int(expat.sum()))
    rule("HR-05", "الاتساق", "Consistency", "لا يوجد تاريخ إقامة لموظف سعودي", "No iqama date on Saudi records",
         ((~expat) & emp["iqama_expiry"].notna()).sum(), "حذف القيمة غير المنطقية", "Clear invalid value", checked=int((~expat).sum()))
    sec_ok = ~(((emp["section"] == "Boys section") & (emp["gender"] != "M")) |
               (emp["section"].isin(["Girls section", "Kindergarten"]) & (emp["gender"] != "F")))
    rule("HR-06", "الاتساق", "Consistency", "جنس المعلم متوافق مع القسم", "Teacher gender matches section",
         (~sec_ok).sum(), "مراجعة التكليف", "Review assignment")
    rule("HR-07", "الصحة", "Validity", "العمر 18–65 وتاريخ التعيين غير مستقبلي", "Age 18–65 and hire date not in future",
         ((emp["age"] < 18) | (emp["age"] > 65) | (emp["hire_date"] > TODAY)).sum(), "التحقق من ملف الموظف", "Verify employee file")
    approved = struct.groupby("nationality")["count"].sum()
    actual = emp.groupby("nationality").size().reindex(approved.index, fill_value=0)
    rule("HR-08", "الاتساق", "Consistency", "العدد الفعلي مطابق للهيكل المعتمد لكل جنسية", "Headcount reconciles to approved structure",
         int((actual != approved).sum()), "مطابقة مع مسيّر الرواتب ومدد", "Reconcile with payroll & Mudad", checked=len(approved))
    return pd.DataFrame(rules)


# ---------------------------------------------------------------- helpers
def band_age(a):
    return "<30" if a < 30 else "30–39" if a < 40 else "40–49" if a < 50 else "50–59" if a < 60 else "60+"


def band_tenure(t):
    return "<1" if t < 1 else "1–3" if t < 3 else "3–5" if t < 5 else "5–10" if t < 10 else "10+"


def month_idx(d):
    """0..11 = months ahead (Oct 2026 … Sep 2027); -1 outside the 12-month window."""
    if d is None or pd.isna(d):
        return -1
    m = (d.year - TODAY.year) * 12 + (d.month - TODAY.month)
    return m - 1 if 1 <= m <= 12 else (0 if m == 0 and d >= TODAY else -1)


# ---------------------------------------------------------------- 3-year plan model
def project(base, retire, a, share=None):
    """Deterministic workforce projection. base[f][g] = headcount (g in SA / EX)."""
    share = a["saudi_share_new_hires"] if share is None else share
    e, out = a["enrollment_growth"], []
    H = {f: dict(v) for f, v in base.items()}
    D0 = {f: v["SA"] + v["EX"] for f, v in base.items()}
    for y, yr in enumerate(YEARS, start=1):
        row = dict(year=yr, fam={}, hires=0.0, saudi_hires=0.0, leavers=0.0)
        for f in H:
            if f == "SUP" and a["outsource_support"] >= 1:
                lv = H[f]["SA"] + H[f]["EX"]
                row["fam"][f] = dict(demand=0, leavers=lv, hires=0, saudi_hires=0, SA=0, EX=0)
                H[f] = {"SA": 0.0, "EX": 0.0}; row["leavers"] += lv
                continue
            demand = D0[f] * (1 + e * GROWTH_FACTOR[f]) ** y
            lv = {g: min(H[f][g], H[f][g] * a["attrition_saudi" if g == "SA" else "attrition_expat"] + retire[f][g][y - 1]) for g in ("SA", "EX")}
            rem = {g: H[f][g] - lv[g] for g in lv}
            need = max(0.0, demand - rem["SA"] - rem["EX"])
            s = need * (1.0 if f in SAUDI_ONLY else share if f in LOCALIZABLE else 0.0)
            H[f] = {"SA": rem["SA"] + s, "EX": rem["EX"] + need - s}
            row["fam"][f] = dict(demand=demand, leavers=lv["SA"] + lv["EX"], hires=need, saudi_hires=s, **H[f])
            row["hires"] += need; row["saudi_hires"] += s; row["leavers"] += lv["SA"] + lv["EX"]
        tot = sum(H[f]["SA"] + H[f]["EX"] for f in H)
        row["total"] = tot
        row["saudi"] = sum(H[f]["SA"] for f in H)
        row["saudization"] = 100 * row["saudi"] / tot if tot else 0
        out.append(row)
    return out


def min_share_for_target(base, retire, a):
    lo, hi = 0.0, 1.0
    if project(base, retire, a, 1.0)[-1]["saudization"] < 100 * a["saudization_target"]:
        return None
    for _ in range(30):
        mid = (lo + hi) / 2
        if project(base, retire, a, mid)[-1]["saudization"] >= 100 * a["saudization_target"]:
            hi = mid
        else:
            lo = mid
    return hi


# ---------------------------------------------------------------- main analytics
def analyse(emp, a):
    emp = emp.copy()
    emp["tenure"] = emp["hire_date"].map(lambda d: (TODAY - d).days / 365.25)
    emp["age_band"] = emp["age"].map(band_age)
    emp["tenure_band"] = emp["tenure"].map(band_tenure)
    emp["grp"] = np.where(emp["nationality"] == "SA", "SA", "EX")
    emp["c_m"] = emp["contract_end"].map(month_idx)
    emp["i_m"] = emp["iqama_expiry"].map(month_idx)

    # register for client-side filtering (no pay, no identifiers)
    register = emp[["nationality", "job_family", "gender", "section", "age_band", "tenure_band", "c_m", "i_m"]].values.tolist()

    # baseline for the plan model
    base = {f: {"SA": 0.0, "EX": 0.0} for f in FAMILIES}
    for r in emp.itertuples():
        base[r.job_family][r.grp] += 1
    retire = {f: {g: [0, 0, 0] for g in ("SA", "EX")} for f in FAMILIES}
    ra = a["retirement_age"]
    for r in emp.itertuples():
        for y in range(1, 4):
            if r.age + y >= ra > r.age + y - 1:
                retire[r.job_family][r.grp][y - 1] += 1

    # pay analytics with small-cell suppression (FIN + HRM merged)
    emp["cost_group"] = emp["job_family"] + "_" + emp["grp"]
    counts = emp["cost_group"].value_counts()
    small = counts[counts < MIN_CELL].index
    emp.loc[emp["cost_group"].isin(small) & emp["job_family"].isin(["FIN", "HRM"]), "cost_group"] = "FINHR_SA"
    total_pay = emp["monthly_package"].sum()
    share = (emp.groupby("cost_group")["monthly_package"].sum() / total_pay * 100).round(1).to_dict()
    share_nat = (emp.groupby("nationality")["monthly_package"].sum() / total_pay * 100).round(1).to_dict()
    per_head_index = (emp.groupby("cost_group")["monthly_package"].mean() / (total_pay / 100)).to_dict()
    weights = {}
    for f in FAMILIES:
        for g in ("SA", "EX"):
            key = f"{f}_{g}" if f"{f}_{g}" in per_head_index else ("FINHR_SA" if f in ("FIN", "HRM") and g == "SA" else None)
            if key is None:  # families with no expatriates today: assume the family average
                fam = [k for k in per_head_index if k.startswith(f + "_")] or ["FINHR_SA"]
                key = fam[0]
            weights[f"{f}_{g}"] = round(per_head_index[key], 4)

    plan = project(base, retire, a)
    need_share = min_share_for_target(base, retire, a)

    # ---- insights
    n = len(emp); sa = int((emp["nationality"] == "SA").sum()); eg = int((emp["nationality"] == "EG").sum())
    tch = int((emp["job_family"] == "TCH").sum()); tch_eg = int(((emp["job_family"] == "TCH") & (emp["nationality"] == "EG")).sum())
    renew90 = int(emp["c_m"].between(0, 2).sum() + emp["i_m"].between(0, 2).sum())   # next three calendar months
    ev = pd.concat([emp.loc[emp["c_m"] >= 0, "c_m"], emp.loc[emp["i_m"] >= 0, "i_m"]])
    peak_m = int(ev.value_counts().idxmax()); peak_n = int(ev.value_counts().max())
    months_ar = ["أكتوبر 2026", "نوفمبر 2026", "ديسمبر 2026", "يناير 2027", "فبراير 2027", "مارس 2027", "أبريل 2027", "مايو 2027", "يونيو 2027", "يوليو 2027", "أغسطس 2027", "سبتمبر 2027"]
    months_en = ["Oct 2026", "Nov 2026", "Dec 2026", "Jan 2027", "Feb 2027", "Mar 2027", "Apr 2027", "May 2027", "Jun 2027", "Jul 2027", "Aug 2027", "Sep 2027"]
    y3 = plan[-1]; hires3 = sum(p["hires"] for p in plan); sh3 = sum(p["saudi_hires"] for p in plan)
    target = 100 * a["saudization_target"]
    insights = [
        dict(kind="gap", ar=f"نسبة التوطين الحالية {100*sa/n:.1f}% ({sa} من {n}) — أولوية تخطيطية للسنوات الثلاث القادمة.",
             en=f"Saudization currently stands at {100*sa/n:.1f}% ({sa} of {n}) — the planning priority for the next three years."),
        dict(kind="subject", ar=f"المصريون {100*eg/n:.1f}% من القوى العاملة، ويشغلون {tch_eg} من {tch} وظيفة تعليمية — تركّز يتطلب خطة إحلال وتعاقب.",
             en=f"Egyptian nationals are {100*eg/n:.1f}% of the workforce and hold {tch_eg} of {tch} teaching posts — a concentration that calls for succession and localisation planning."),
        dict(kind="priority", ar=f"{renew90} تجديدًا (عقود وإقامات) يستحق خلال الأشهر الثلاثة القادمة، وذروة التجديدات في {months_ar[peak_m]} بعدد {peak_n}.",
             en=f"{renew90} renewals (contracts and iqamas) fall due in the next three months; the peak is {months_en[peak_m]} with {peak_n}."),
        dict(kind="up", ar=f"وفق السيناريو الأساسي (نمو الطلاب {a['enrollment_growth']*100:.0f}% سنويًا) يصل العدد إلى {y3['total']:.0f} موظفًا بنهاية {YEARS[-1]} مع {hires3:.0f} تعيينًا خلال ثلاث سنوات، منها {sh3:.0f} سعوديًا.",
             en=f"Under the base scenario ({a['enrollment_growth']*100:.0f}% annual enrolment growth) headcount reaches {y3['total']:.0f} by {YEARS[-1]}, with {hires3:.0f} hires over three years — {sh3:.0f} of them Saudi."),
        dict(kind="forecast", ar=(f"التوطين المتوقع بنهاية السنة الثالثة {y3['saudization']:.1f}% مقابل مستهدف {target:.0f}%؛ ويتطلب بلوغه ألا تقل نسبة السعوديين من تعيينات التعليم والنقل عن {need_share*100:.0f}%." if need_share is not None else f"المستهدف {target:.0f}% لا يتحقق بالتعيين الجديد وحده ويتطلب إحلالًا مباشرًا."),
             en=(f"Projected Saudization at end of Year 3 is {y3['saudization']:.1f}% against a {target:.0f}% target; reaching it requires at least {need_share*100:.0f}% of new teaching and transport hires to be Saudi." if need_share is not None else f"The {target:.0f}% target cannot be reached through new hiring alone and needs direct replacement.")),
        dict(kind="subject", ar="سائقو الحافلات (14) وعمال النظافة (3) جميعهم غير سعوديين: الأولى مرشحة للتوطين التدريجي، والثانية للإسناد لمقاول خدمات.",
             en="All 14 bus drivers and 3 cleaners are expatriates: drivers are a phased-localisation candidate; cleaning is a candidate for outsourcing."),
    ]

    fam_counts = emp.groupby(["job_family", "nationality"]).size().unstack(fill_value=0)
    return dict(
        register=register, base=base, retire=retire, weights=weights, plan=plan, need_share=need_share,
        pay_share=share, pay_share_nat=share_nat, insights=insights,
        stats=dict(headcount=n, saudi=sa, avg_age=round(emp["age"].mean(), 1), avg_tenure=round(emp["tenure"].mean(), 1),
                   under1=int((emp["tenure"] < 1).sum()), renew90=renew90,
                   fam_nat={f: {k: int(v) for k, v in fam_counts.loc[f].items()} for f in fam_counts.index}),
    )


def main():
    emp, struct, a = load()
    dq = data_quality(emp, struct)
    res = analyse(emp, a)
    feed = dict(
        meta=dict(title_ar="تحليلات تخطيط القوى العاملة — شركة صني فالي العالمية", title_en="Workforce Planning Analytics — Sunny Valley International",
                  as_of=TODAY.isoformat(), years=YEARS, generated=datetime.now(timezone.utc).strftime("%Y-%m-%d")),
        labels=dict(fam={k: dict(ar=v[0], en=v[1]) for k, v in FAMILIES.items()}, nat={k: dict(ar=v[0], en=v[1]) for k, v in NATS.items()}),
        assumptions=a, localizable=sorted(LOCALIZABLE), saudi_only=sorted(SAUDI_ONLY), growth_factor=GROWTH_FACTOR,
        dq=dict(rules=dq.to_dict("records"), overall=round(float(dq["pass_rate"].mean()), 1)),
        **{k: res[k] for k in ["register", "base", "retire", "weights", "plan", "need_share", "pay_share", "pay_share_nat", "insights", "stats"]},
    )
    (OUT / "hr_dashboard.json").write_text(json.dumps(feed, ensure_ascii=False, default=float, separators=(",", ":")), encoding="utf-8")
    dq.to_csv(OUT / "dq_report.csv", index=False, encoding="utf-8-sig")
    print("DQ:", dq[["id", "failed"]].values.tolist())
    for p in res["plan"]:
        print(p["year"], round(p["total"], 1), f"{p['saudization']:.1f}%", "hires", round(p["hires"], 1), "saudi", round(p["saudi_hires"], 1))
    print("need share:", res["need_share"]); print("pay share:", res["pay_share"])
    for i in res["insights"]:
        print(" •", i["en"])


if __name__ == "__main__":
    main()
