from flask import Blueprint, request, render_template, redirect, url_for, flash, jsonify
from flask_login import login_required, current_user
from extensions import db
from models import (
    Employee, Department,
    IncentivePolicy, IncentiveTarget, IncentiveScheme, IncentiveDistributionRule,
    IncentiveCalculationBatch, IncentiveCalculationLine, IncentivePayment
)
from utils.incentive_utils import safe_eval
from datetime import datetime, date

incentive_bp = Blueprint("incentives", __name__, url_prefix="/incentives")


# -------------------------
# Access Helpers
# -------------------------
def _role_name():
    return (getattr(getattr(current_user, "role", None), "name", "") or "").lower()

def _hr_admin_only():
    return _role_name() in ("admin", "hr", "hr admin")

def _can_disburse(mode: str) -> bool:
    """
    You can tighten this later using your RoleAccess/UserAccess module.
    For now: HR/Admin can disburse.
    """
    if not _hr_admin_only():
        return False
    return mode in ("cash", "bank")


# -------------------------
# Policies
# -------------------------
@incentive_bp.route("/policies", methods=["GET", "POST"])
@login_required
def policies():
    if request.method == "POST":
        if not _hr_admin_only():
            flash("❌ Access denied.", "danger")
            return redirect(url_for("incentives.policies"))

        name = (request.form.get("name") or "").strip()
        if not name:
            flash("Name is required.", "danger")
            return redirect(url_for("incentives.policies"))

        p = IncentivePolicy(
            name=name,
            description=request.form.get("description"),
            period_type=request.form.get("period_type") or "monthly",
            allow_cash=(request.form.get("allow_cash") is not None),
            allow_bank=(request.form.get("allow_bank") is not None),
            eligibility_json={"include_all": True},
            status="draft",
            created_by=current_user.id,
        )
        db.session.add(p)
        db.session.commit()
        flash("✅ Incentive Policy created.", "success")
        return redirect(url_for("incentives.policy_detail", policy_id=p.id))

    policies = IncentivePolicy.query.order_by(IncentivePolicy.id.desc()).all()
    return render_template("incentives/policies.html", policies=policies)


@incentive_bp.route("/policies/<int:policy_id>", methods=["GET"])
@login_required
def policy_detail(policy_id):
    p = IncentivePolicy.query.get_or_404(policy_id)

    departments = Department.query.order_by(Department.name.asc()).all()

    # distinct locations / institutes from active employees
    locations = [
        r[0] for r in db.session.query(Employee.location)
        .filter(Employee.status == "active", Employee.location.isnot(None), Employee.location != "")
        .distinct().order_by(Employee.location.asc()).all()
    ]

    institutes = [
        r[0] for r in db.session.query(Employee.institute)
        .filter(Employee.status == "active", Employee.institute.isnot(None), Employee.institute != "")
        .distinct().order_by(Employee.institute.asc()).all()
    ]

    employees = Employee.query.filter(Employee.status == "active").order_by(Employee.full_name.asc()).all()

    return render_template(
        "incentives/policy_detail.html",
        policy=p,
        departments=departments,
        locations=locations,
        institutes=institutes,
        employees=employees
    )


@incentive_bp.route("/policies/<int:policy_id>/targets", methods=["POST"])
@login_required
def add_target(policy_id):
    if not _hr_admin_only():
        flash("❌ Access denied.", "danger")
        return redirect(url_for("incentives.policy_detail", policy_id=policy_id))

    p = IncentivePolicy.query.get_or_404(policy_id)

    metric_key = (request.form.get("metric_key") or "").strip().lower()

    t = IncentiveTarget(
        policy_id=p.id,
        target_scope=request.form.get("target_scope"),
        scope_ref=request.form.get("scope_ref"),
        metric_key=metric_key,
        metric_source=request.form.get("metric_source") or "manual",
        target_value=float(request.form.get("target_value") or 0),
        unit=request.form.get("unit"),
        eligibility_json={},
    )
    db.session.add(t)
    db.session.commit()
    flash("✅ Target added.", "success")
    return redirect(url_for("incentives.policy_detail", policy_id=p.id))


@incentive_bp.route("/targets/<int:target_id>/schemes", methods=["POST"])
@login_required
def add_scheme(target_id):
    if not _hr_admin_only():
        flash("❌ Access denied.", "danger")
        return redirect(request.referrer or url_for("incentives.policies"))

    t = IncentiveTarget.query.get_or_404(target_id)

    s = IncentiveScheme(
        target_id=t.id,
        reward_type=request.form.get("reward_type"),
        total_amount=float(request.form.get("total_amount") or 0) if request.form.get("total_amount") else None,
        currency=request.form.get("currency") or "INR",
        scheme_json={},
    )
    db.session.add(s)
    db.session.commit()
    flash("✅ Scheme added.", "success")
    return redirect(url_for("incentives.policy_detail", policy_id=t.policy_id))


@incentive_bp.route("/schemes/<int:scheme_id>/rules", methods=["POST"])
@login_required
def add_rule(scheme_id):
    if not _hr_admin_only():
        flash("❌ Access denied.", "danger")
        return redirect(request.referrer or url_for("incentives.policies"))

    s = IncentiveScheme.query.get_or_404(scheme_id)

    distribution_type = request.form.get("distribution_type")
    config_json = request.form.get("config_json")  # raw JSON from UI
    try:
        import json
        parsed = json.loads(config_json) if config_json else {}
    except Exception:
        parsed = {}
        flash("⚠️ Invalid JSON config; saved empty config.", "warning")

    r = IncentiveDistributionRule(
        scheme_id=s.id,
        distribution_type=distribution_type,
        description=request.form.get("description"),
        is_individual_only=(distribution_type == "slab"),
        requires_approval=True,
        config_json=parsed,
    )
    db.session.add(r)
    db.session.commit()
    flash("✅ Distribution rule added.", "success")
    return redirect(url_for("incentives.policy_detail", policy_id=s.target.policy_id))


# -------------------------
# Preview Calculation (JSON)
# -------------------------
@incentive_bp.route("/preview", methods=["POST"])
@login_required
def preview():
    if not _hr_admin_only():
        return jsonify({"ok": False, "error": "Access denied"}), 403

    data = request.get_json(force=True)
    policy_id = int(data.get("policy_id"))
    period_key = data.get("period_key") or datetime.today().strftime("%Y-%m")

    p = IncentivePolicy.query.get_or_404(policy_id)

    # map achievements per employee+metric (normalize metric_key)
    ach_map = {}
    for a in (data.get("achievements") or []):
        emp_id = int(a.get("employee_id"))
        mkey = (a.get("metric_key") or "").strip().lower()
        ach_map[(emp_id, mkey)] = float(a.get("value") or 0)

    lines = []

    for target in p.targets:
        if not target.schemes:
            continue

        scheme = target.schemes[-1]
        if not scheme.distribution_rules:
            continue

        rule = scheme.distribution_rules[-1]
        dist = rule.distribution_type
        cfg = rule.config_json or {}

        tkey = (target.metric_key or "").strip().lower()
        eligible_emp_ids = _resolve_target_employees(target)

        if dist == "equal":
            pool = float(scheme.total_amount or 0)
            n = max(1, len(eligible_emp_ids))
            per = pool / n
            for emp_id in eligible_emp_ids:
                lines.append(_mk_line(emp_id, target, scheme, rule, ach_map, tkey, per,
                                      {"method": "equal", "pool": pool, "n": n}))

        elif dist == "fixed_per_employee":
            per = float(cfg.get("amount") or 0)
            for emp_id in eligible_emp_ids:
                lines.append(_mk_line(emp_id, target, scheme, rule, ach_map, tkey, per,
                                      {"method": "fixed_per_employee", "amount": per}))

        elif dist == "percentage":
            pct = float(cfg.get("percentage") or 0)
            base = float(scheme.total_amount or 0)
            amt = (pct / 100.0) * base
            for emp_id in eligible_emp_ids:
                lines.append(_mk_line(emp_id, target, scheme, rule, ach_map, tkey, amt,
                                      {"method": "percentage", "pct": pct, "base": base}))

        elif dist == "custom_formula":
            formula = (cfg.get("formula") or "").strip()
            defaults = cfg.get("defaults") or {}
            for emp_id in eligible_emp_ids:
                achieved = ach_map.get((emp_id, tkey), 0.0)
                vars_ = {
                    "achievement": achieved,
                    "target": float(target.target_value or 0),
                    "total_amount": float(scheme.total_amount or 0),
                    **defaults
                }
                try:
                    amt = float(safe_eval(formula, vars_)) if formula else 0.0
                except Exception:
                    amt = 0.0
                lines.append(_mk_line(emp_id, target, scheme, rule, ach_map, tkey, amt,
                                      {"method": "custom_formula", "formula": formula, "vars": vars_}))

        elif dist == "slab":
            if target.target_scope != "individual":
                continue

            slabs = sorted((cfg.get("slabs") or []), key=lambda x: float(x.get("from") or 0))
            increment_per_x = float(cfg.get("increment_per_x") or 0)
            increment_amount = float(cfg.get("increment_amount") or 0)

            for emp_id in eligible_emp_ids:
                achieved = ach_map.get((emp_id, tkey), 0.0)

                slab_amt = 0.0
                for srow in slabs:
                    s_from = float(srow.get("from") or 0)
                    s_to = srow.get("to")
                    s_to = float(s_to) if s_to is not None else None
                    s_amt = float(srow.get("amount") or 0)

                    if achieved >= s_from and (s_to is None or achieved <= s_to):
                        slab_amt = s_amt
                        break

                bonus = 0.0
                if increment_per_x > 0 and increment_amount > 0 and achieved > 0:
                    bonus = int(achieved // increment_per_x) * increment_amount

                total = slab_amt + bonus
                lines.append(_mk_line(
                    emp_id, target, scheme, rule, ach_map, tkey, total,
                    {
                        "method": "slab",
                        "achieved": achieved,
                        "slab_amt": slab_amt,
                        "increment_per_x": increment_per_x,
                        "increment_amount": increment_amount,
                        "bonus": bonus
                    }
                ))

        elif dist == "workload_based":
            pool = float(scheme.total_amount or 0)
            weights = cfg.get("weights") or []
            if not weights or pool <= 0:
                continue

            dept_emp_map = {}
            for w in weights:
                try:
                    dept_id = int(w.get("department_id"))
                except Exception:
                    continue
                dept_emp_map[dept_id] = [
                    e.id for e in Employee.query.filter(
                        Employee.department_id == dept_id,
                        Employee.status == "active"
                    ).all()
                ]

            for w in weights:
                try:
                    dept_id = int(w.get("department_id"))
                    wt = float(w.get("weight") or 0)
                except Exception:
                    continue

                dept_pool = pool * wt
                emp_ids = dept_emp_map.get(dept_id, [])
                if not emp_ids:
                    continue

                per = dept_pool / max(1, len(emp_ids))
                for emp_id in emp_ids:
                    lines.append(_mk_line(
                        emp_id, target, scheme, rule, ach_map, tkey, per,
                        {"method": "workload_based", "pool": pool, "dept_id": dept_id, "weight": wt, "dept_pool": dept_pool, "n": len(emp_ids)}
                    ))

    return jsonify({"ok": True, "policy_id": p.id, "period_key": period_key, "lines": lines})


def _mk_line(emp_id, target, scheme, rule, ach_map, tkey, amount, breakup):
    achieved = ach_map.get((emp_id, tkey), 0.0)
    return {
        "employee_id": emp_id,
        "target_id": target.id,
        "scheme_id": scheme.id,
        "rule_id": rule.id,
        "metric_key": tkey,
        "achieved_value": achieved,
        "target_value": float(target.target_value or 0),
        "amount": round(float(amount or 0), 2),
        "breakup": breakup,
    }


def _resolve_target_employees(target: IncentiveTarget):
    scope = target.target_scope
    ref = (target.scope_ref or "").strip()
    q = Employee.query.filter(Employee.status == "active")

    if scope == "individual":
        try:
            return [int(ref)]
        except Exception:
            return []

    if scope == "department":
        try:
            dept_id = int(ref)
            return [e.id for e in q.filter(Employee.department_id == dept_id).all()]
        except Exception:
            return []

    if scope == "location":
        if not ref:
            return []
        return [e.id for e in q.filter(Employee.location == ref).all()]

    if scope == "institute":
        if not ref:
            return []
        return [e.id for e in q.filter(Employee.institute == ref).all()]

    # group: keep generic for now (later we can add group table/mapping)
    return [e.id for e in q.all()]


# -------------------------
# Batch Freeze / Approve / Paid
# -------------------------
@incentive_bp.route("/batches", methods=["GET"])
@login_required
def list_batches():
    if not _hr_admin_only():
        flash("❌ Access denied.", "danger")
        return redirect(url_for("dashboard.dashboard"))

    batches = IncentiveCalculationBatch.query.order_by(IncentiveCalculationBatch.id.desc()).all()
    policies = IncentivePolicy.query.order_by(IncentivePolicy.name.asc()).all()
    return render_template("incentives/batches.html", batches=batches, policies=policies)


@incentive_bp.route("/batches/<int:batch_id>", methods=["GET"])
@login_required
def view_batch(batch_id):
    if not _hr_admin_only():
        flash("❌ Access denied.", "danger")
        return redirect(url_for("dashboard.dashboard"))

    b = IncentiveCalculationBatch.query.get_or_404(batch_id)
    policy = IncentivePolicy.query.get(b.policy_id)
    lines = IncentiveCalculationLine.query.filter_by(batch_id=b.id).order_by(IncentiveCalculationLine.id.asc()).all()

    total_amt = sum((l.calculated_amount or 0) for l in lines)

    return render_template("incentives/batch_detail.html", batch=b, policy=policy, lines=lines, total_amt=total_amt)


@incentive_bp.route("/batches/create-from-preview", methods=["POST"])
@login_required
def create_batch_from_preview():
    if not _hr_admin_only():
        return jsonify({"ok": False, "error": "Access denied"}), 403

    payload = request.get_json(force=True)
    policy_id = int(payload.get("policy_id"))
    period_key = (payload.get("period_key") or datetime.today().strftime("%Y-%m")).strip()
    overwrite = bool(payload.get("overwrite", True))

    preview_resp = preview()
    if isinstance(preview_resp, tuple):
        return preview_resp
    preview_json = preview_resp.get_json()

    if not preview_json.get("ok"):
        return jsonify({"ok": False, "error": "Preview failed"}), 400

    lines = preview_json.get("lines") or []
    IncentivePolicy.query.get_or_404(policy_id)

    existing = IncentiveCalculationBatch.query.filter_by(
        policy_id=policy_id, period_key=period_key, status="draft"
    ).first()

    if existing and overwrite:
        IncentiveCalculationLine.query.filter_by(batch_id=existing.id).delete()
        db.session.delete(existing)
        db.session.commit()
        existing = None

    if existing:
        batch = existing
    else:
        batch = IncentiveCalculationBatch(
            policy_id=policy_id,
            period_key=period_key,
            status="draft",
            inputs_json={"achievements": payload.get("achievements") or []},
            created_by=current_user.id,
        )
        db.session.add(batch)
        db.session.flush()

    for row in lines:
        il = IncentiveCalculationLine(
            batch_id=batch.id,
            employee_id=int(row.get("employee_id")),
            target_id=row.get("target_id"),
            scheme_id=row.get("scheme_id"),
            rule_id=row.get("rule_id"),
            achieved_value=float(row.get("achieved_value") or 0),
            target_value=float(row.get("target_value") or 0),
            calculated_amount=float(row.get("amount") or 0),
            formula_used=(row.get("breakup") or {}).get("formula"),
            calc_breakup_json=row.get("breakup") or {},
            status="draft",
        )
        db.session.add(il)

    db.session.commit()
    return jsonify({"ok": True, "batch_id": batch.id, "redirect": url_for("incentives.view_batch", batch_id=batch.id)})


@incentive_bp.route("/batches/<int:batch_id>/approve", methods=["POST"])
@login_required
def approve_batch(batch_id):
    if not _hr_admin_only():
        flash("❌ Access denied.", "danger")
        return redirect(url_for("incentives.view_batch", batch_id=batch_id))

    b = IncentiveCalculationBatch.query.get_or_404(batch_id)
    if b.status != "draft":
        flash("⚠️ Only draft batches can be approved.", "warning")
        return redirect(url_for("incentives.view_batch", batch_id=batch_id))

    b.status = "approved"
    IncentiveCalculationLine.query.filter_by(batch_id=b.id).update({"status": "approved"})
    db.session.commit()

    flash("✅ Batch approved (locked).", "success")
    return redirect(url_for("incentives.view_batch", batch_id=b.id))


@incentive_bp.route("/batches/<int:batch_id>/mark-paid", methods=["POST"])
@login_required
def mark_paid(batch_id):
    if not _hr_admin_only():
        flash("❌ Access denied.", "danger")
        return redirect(url_for("incentives.view_batch", batch_id=batch_id))

    mode = (request.form.get("payment_mode") or "").strip().lower()
    if mode not in ("cash", "bank"):
        flash("❌ Invalid payment mode.", "danger")
        return redirect(url_for("incentives.view_batch", batch_id=batch_id))

    if not _can_disburse(mode):
        flash("❌ You are not allowed to disburse this payment mode.", "danger")
        return redirect(url_for("incentives.view_batch", batch_id=batch_id))

    b = IncentiveCalculationBatch.query.get_or_404(batch_id)
    if b.status != "approved":
        flash("⚠️ Only approved batches can be marked paid.", "warning")
        return redirect(url_for("incentives.view_batch", batch_id=batch_id))

    policy = IncentivePolicy.query.get(b.policy_id)
    if mode == "cash" and not policy.allow_cash:
        flash("❌ Cash incentive not allowed for this policy.", "danger")
        return redirect(url_for("incentives.view_batch", batch_id=batch_id))
    if mode == "bank" and not policy.allow_bank:
        flash("❌ Bank incentive not allowed for this policy.", "danger")
        return redirect(url_for("incentives.view_batch", batch_id=batch_id))

    lines = IncentiveCalculationLine.query.filter_by(batch_id=b.id).all()

    already_paid = IncentivePayment.query.join(
        IncentiveCalculationLine, IncentivePayment.calc_line_id == IncentiveCalculationLine.id
    ).filter(IncentiveCalculationLine.batch_id == b.id).first()

    if already_paid:
        flash("⚠️ Payments already created for this batch.", "warning")
        return redirect(url_for("incentives.view_batch", batch_id=b.id))

    today = date.today()
    ref = (request.form.get("reference") or "").strip()
    notes = (request.form.get("notes") or "").strip()

    for l in lines:
        pay = IncentivePayment(
            calc_line_id=l.id,
            payment_mode=mode,
            paid_on=today,
            reference=ref or None,
            notes=notes or None
        )
        db.session.add(pay)
        l.status = "paid"

    b.status = "paid"
    db.session.commit()

    flash(f"✅ Batch marked as paid ({mode}).", "success")
    return redirect(url_for("incentives.view_batch", batch_id=b.id))


@incentive_bp.route("/run", methods=["GET"])
@login_required
def run_incentive():
    if not _hr_admin_only():
        flash("❌ Access denied.", "danger")
        return redirect(url_for("dashboard.dashboard"))

    policies = IncentivePolicy.query.order_by(IncentivePolicy.id.desc()).all()
    default_period = datetime.today().strftime("%Y-%m")

    departments = Department.query.order_by(Department.name.asc()).all()

    locations = [r[0] for r in db.session.query(Employee.location)
                 .filter(Employee.status == "active", Employee.location.isnot(None), Employee.location != "")
                 .distinct().order_by(Employee.location.asc()).all()]

    institutes = [r[0] for r in db.session.query(Employee.institute)
                  .filter(Employee.status == "active", Employee.institute.isnot(None), Employee.institute != "")
                  .distinct().order_by(Employee.institute.asc()).all()]

    return render_template(
        "incentives/run_incentive.html",
        policies=policies,
        default_period=default_period,
        departments=departments,
        locations=locations,
        institutes=institutes
    )


@incentive_bp.route("/ajax/employees", methods=["GET"])
@login_required
def ajax_employees():
    if not _hr_admin_only():
        return jsonify({"ok": False, "error": "Access denied"}), 403

    dept_id = (request.args.get("department_id") or "").strip()
    location = (request.args.get("location") or "").strip()
    institute = (request.args.get("institute") or "").strip()

    q = Employee.query.filter(Employee.status == "active")

    if dept_id:
        try:
            q = q.filter(Employee.department_id == int(dept_id))
        except Exception:
            pass

    if location:
        q = q.filter(Employee.location == location)

    if institute:
        q = q.filter(Employee.institute == institute)

    employees = q.order_by(Employee.full_name.asc()).all()

    return jsonify({
        "ok": True,
        "employees": [
            {"id": e.id, "full_name": e.full_name, "employee_code": e.employee_code}
            for e in employees
        ]
    })

@incentive_bp.route("/ajax/policy-metrics", methods=["GET"])
@login_required
def ajax_policy_metrics():
    if not _hr_admin_only():
        return jsonify({"ok": False, "error": "Access denied"}), 403

    policy_id = (request.args.get("policy_id") or "").strip()
    if not policy_id:
        return jsonify({"ok": True, "metrics": []})

    try:
        pid = int(policy_id)
    except Exception:
        return jsonify({"ok": True, "metrics": []})

    # Collect unique metric keys from targets of the policy
    rows = (
        db.session.query(IncentiveTarget.metric_key)
        .filter(IncentiveTarget.policy_id == pid)
        .filter(IncentiveTarget.metric_key.isnot(None), IncentiveTarget.metric_key != "")
        .distinct()
        .order_by(IncentiveTarget.metric_key.asc())
        .all()
    )

    metrics = []
    for (mk,) in rows:
        key = (mk or "").strip().lower()
        if key:
            metrics.append({"key": key, "label": key})

    return jsonify({"ok": True, "policy_id": pid, "metrics": metrics})