"""
CA Automator — Transfer Pricing computation engines.

Pure functions only (no DB access, no HTTP) so they can be unit-tested in
isolation and mirrored line-for-line in web/tp_engines.js, same discipline as
every other engine pair in this app. Every function here produces a
*starting analysis* for a qualified professional to review, never a final
legal or economic conclusion — that framing is carried in the returned
"basis"/"explanation" text deliberately, not just in this docstring.

Nothing here embeds a country's substantive law. Country-specific figures
(thresholds, tax rates, methods a jurisdiction actually accepts) come from
the tp_regulatory_rule table via the caller — this module only knows generic
transfer-pricing arithmetic and a jurisdiction-agnostic method heuristic.
"""

R2 = lambda x: round(float(x or 0), 2)


# ------------------------------------------------------------- benchmarking

def compute_benchmark_stats(comparables):
    """comparables: [{"value": float, "weight": float (optional, default 1)}, ...]
    Returns min/max/mean/median/q1/q3/iqr, weighted average/median, and an
    outlier-trimmed "adjusted range" (drops the top and bottom decile when
    there are enough points to do so meaningfully — a common, but not
    universal, practitioner convention; always shown alongside the untrimmed
    range, never in its place)."""
    values = sorted(float(c["value"]) for c in comparables if c.get("value") is not None)
    n = len(values)
    if n == 0:
        return {"count": 0, "min": None, "max": None, "mean": None, "median": None,
                "q1": None, "q3": None, "iqr": None, "weighted_average": None,
                "weighted_median": None, "adjusted_range": None}

    def percentile(sorted_vals, pct):
        if len(sorted_vals) == 1:
            return sorted_vals[0]
        k = (len(sorted_vals) - 1) * pct
        f, c = int(k), min(int(k) + 1, len(sorted_vals) - 1)
        if f == c:
            return sorted_vals[f]
        return sorted_vals[f] + (sorted_vals[c] - sorted_vals[f]) * (k - f)

    mean = sum(values) / n
    median = percentile(values, 0.5)
    q1, q3 = percentile(values, 0.25), percentile(values, 0.75)
    iqr = q3 - q1

    weights = [float(c.get("weight") or 1) for c in comparables if c.get("value") is not None]
    total_w = sum(weights)
    weighted_average = (sum(v * w for v, w in zip(values, weights)) / total_w) if total_w else None
    weighted_median = _weighted_percentile(values, weights, 0.5) if total_w else None

    if n >= 5:
        trim = max(1, round(n * 0.1))
        trimmed = values[trim:n - trim] if n - 2 * trim >= 1 else values
        adjusted_range = {"min": R2(trimmed[0]), "max": R2(trimmed[-1])}
    else:
        adjusted_range = None

    return {
        "count": n, "min": R2(values[0]), "max": R2(values[-1]), "mean": R2(mean),
        "median": R2(median), "q1": R2(q1), "q3": R2(q3), "iqr": R2(iqr),
        "weighted_average": R2(weighted_average) if weighted_average is not None else None,
        "weighted_median": R2(weighted_median) if weighted_median is not None else None,
        "adjusted_range": adjusted_range,
    }


def _weighted_percentile(sorted_values, weights, pct):
    # values must already be sorted ascending, weights in the same order.
    pairs = sorted(zip(sorted_values, weights), key=lambda p: p[0])
    total = sum(w for _, w in pairs)
    if total <= 0:
        return None
    cum = 0.0
    target = pct * total
    for v, w in pairs:
        cum += w
        if cum >= target:
            return v
    return pairs[-1][0]


def working_capital_adjustment(comparable, tested_party, financing_rate_pct):
    """A standard simplified working-capital adjustment: converts the
    difference in net-working-capital-to-sales ratios between the tested
    party and a comparable into a margin adjustment, at the given financing
    rate. `comparable`/`tested_party`: {"receivables", "inventory",
    "payables", "sales"}. This is one widely-used simplification among
    several defensible approaches — a professional judgment call on which
    formula and financing rate to use is still required, this just does the
    arithmetic once that's decided."""
    def nwc_ratio(d):
        sales = float(d.get("sales") or 0)
        if not sales:
            return None
        nwc = float(d.get("receivables") or 0) + float(d.get("inventory") or 0) - float(d.get("payables") or 0)
        return nwc / sales

    tp_ratio, comp_ratio = nwc_ratio(tested_party), nwc_ratio(comparable)
    if tp_ratio is None or comp_ratio is None:
        return {"adjustment_pct": None, "tested_party_nwc_ratio": None, "comparable_nwc_ratio": None,
                "note": "Sales figure missing for the tested party or the comparable — cannot compute a ratio."}
    rate = float(financing_rate_pct or 0) / 100
    adjustment_pct = rate * (tp_ratio - comp_ratio)
    return {"adjustment_pct": R2(adjustment_pct * 100), "tested_party_nwc_ratio": R2(tp_ratio * 100),
            "comparable_nwc_ratio": R2(comp_ratio * 100),
            "note": "Added to the comparable's margin. Positive means the tested party carries more "
                    "working-capital-related financing burden than the comparable."}


# --------------------------------------------------------- method selection

_METHOD_LABELS = {
    "cup": "Comparable Uncontrolled Price (CUP)", "rpm": "Resale Price Method",
    "cpm": "Cost Plus Method", "tnmm": "Transactional Net Margin Method (TNMM)",
    "psm": "Profit Split Method", "other": "Other method",
}


def recommend_method(transaction, far, comparables_available):
    """transaction: {"transaction_category", "transaction_subtype"}
    far: {"functional_profile"}
    comparables_available: {"internal_cup": bool, "external_gross_margin": bool,
                             "external_net_margin": bool, "unique_intangible": bool}
    Returns {"recommended_method", "label", "basis"} — a starting suggestion
    only; the preparer sees the reasoning and can always override with their
    own justification (that override is recorded by the caller, not here)."""
    cat = (transaction.get("transaction_category") or "").lower()
    profile = (far.get("functional_profile") or "").lower()
    ca = comparables_available or {}
    reasons = []

    if ca.get("internal_cup"):
        reasons.append("An internal comparable uncontrolled transaction is available — CUP is generally the most "
                        "direct method when a reliable internal or external comparable price exists.")
        return {"recommended_method": "cup", "label": _METHOD_LABELS["cup"], "basis": " ".join(reasons)}

    if cat in ("financing", "loan", "guarantee"):
        reasons.append("Financing/guarantee arrangements are typically benchmarked against comparable "
                        "uncontrolled interest rates or fee rates (a CUP-style approach), subject to data availability.")
        return {"recommended_method": "cup", "label": _METHOD_LABELS["cup"], "basis": " ".join(reasons)}

    if ca.get("unique_intangible") or cat == "royalty" and profile in ("ip_owner", "entrepreneurial"):
        reasons.append("A unique or valuable intangible with no reliable comparable suggests a profit-based "
                        "approach — Profit Split is often considered when both parties make unique, valuable contributions.")
        return {"recommended_method": "psm", "label": _METHOD_LABELS["psm"], "basis": " ".join(reasons)}

    if profile in ("routine_distributor", "limited_risk_distributor") and cat == "goods":
        if ca.get("external_gross_margin"):
            reasons.append("A limited-risk distributor reselling goods, with external gross-margin comparables "
                            "available, is a classic Resale Price Method fact pattern.")
            return {"recommended_method": "rpm", "label": _METHOD_LABELS["rpm"], "basis": " ".join(reasons)}
        reasons.append("A limited-risk distributor fact pattern, but reliable gross-margin comparables were not "
                        "indicated — TNMM (net margin) is more commonly applied in practice when only net-margin "
                        "data is available.")
        return {"recommended_method": "tnmm", "label": _METHOD_LABELS["tnmm"], "basis": " ".join(reasons)}

    if profile in ("contract_manufacturer",) and ca.get("external_gross_margin"):
        reasons.append("A contract manufacturer with a cost-plus fact pattern and external gross-margin/mark-up "
                        "comparables available — Cost Plus is the classic method here.")
        return {"recommended_method": "cpm", "label": _METHOD_LABELS["cpm"], "basis": " ".join(reasons)}

    reasons.append("No internal CUP, no reliable gross-margin comparable set, and/or the functional profile "
                    "doesn't point clearly to CUP/RPM/CPM/PSM — TNMM is the most commonly applied method in "
                    "practice for routine functions where only net-margin comparable data is available.")
    return {"recommended_method": "tnmm", "label": _METHOD_LABELS["tnmm"], "basis": " ".join(reasons)}


# --------------------------------------------------------------- risk scoring

_RISK_LEVELS = ["Low", "Medium", "High", "Very High"]


def score_risk(transaction, far, benchmark, flags=None):
    """transaction: {"amount"}; far: {"functional_profile"} (presence/absence
    used as a documentation-completeness signal); benchmark: {"stats":
    {"count"...}, "actual_result", "in_range": bool|None}; flags (all
    optional bools): related_party_pct_high, loss_making, persistent_losses,
    cross_border, sensitive_category (royalty/management fee/guarantee/
    interest-free), documentation_gap. Returns the point-scored factors
    alongside the level so nothing is a bare, unexplained label."""
    flags = flags or {}
    factors = []

    def add(name, hit, points, note):
        if hit:
            factors.append({"factor": name, "points": points, "note": note})

    amount = float(transaction.get("amount") or 0)
    add("Large transaction value", amount >= 10_000_000, 2, "Transaction value is NPR/local-currency-equivalent 1 crore or more (illustrative threshold, not a statutory one).")
    add("Moderate transaction value", 1_000_000 <= amount < 10_000_000, 1, "Transaction value is material but below the large-transaction band.")

    stats = (benchmark or {}).get("stats") or {}
    comp_count = stats.get("count") or 0
    add("Few or no comparables", comp_count < 3, 3, f"Only {comp_count} comparable(s) in the accepted set — a thin benchmark set is inherently less reliable.")
    add("Result outside the benchmarked range", (benchmark or {}).get("in_range") is False, 3, "The actual result falls outside the computed arm's-length range.")

    add("Loss-making tested party", bool(flags.get("loss_making")), 2, "The tested party reported a loss for the period.")
    add("Persistent losses", bool(flags.get("persistent_losses")), 2, "The tested party has reported losses across multiple consecutive periods — a recognised risk indicator in practice.")
    add("High related-party transaction concentration", bool(flags.get("related_party_pct_high")), 1, "Related-party transactions form a large share of the entity's total transactions.")
    add("Cross-border transaction", bool(flags.get("cross_border")), 1, "Source and destination countries differ, adding a second jurisdiction's rules to consider.")
    add("Historically scrutinised category", bool(flags.get("sensitive_category")), 1, "Categories such as management fees, royalties, guarantees and interest-free financing draw closer scrutiny in practice across all three jurisdictions.")
    add("Documentation gap", bool(flags.get("documentation_gap")) or not far.get("functional_profile"), 2, "FAR analysis or supporting documentation is incomplete for this transaction.")

    total = sum(f["points"] for f in factors)
    if total >= 8:
        level = "Very High"
    elif total >= 5:
        level = "High"
    elif total >= 2:
        level = "Medium"
    else:
        level = "Low"

    return {"risk_level": level, "score": total, "factors": factors,
            "explanation": f"{level} risk — {len(factors)} contributing factor(s) totalling {total} point(s). "
                           "This is a triage aid to prioritise review, not a conclusion on whether an adjustment is warranted."}


# --------------------------------------------------------- adjustment exposure

def compute_adjustment_exposure(transaction, benchmark, tax_rate_pct=None, penalty_pct=None, interest_rate_pct=None,
                                 periods=1, arm_length_result=None):
    """actual_result and a chosen arm's-length point (the benchmark median by
    default, or `arm_length_result` when the caller supplies one explicitly —
    see compute_scenarios below, which passes Q1/median/Q3/a user figure in
    turn) are both margins/rates on the SAME base as `base_amount` (e.g. both
    % margins on sales, both interest rates on a loan principal) — the caller
    is responsible for putting them on a comparable footing. tax_rate_pct/
    penalty_pct/interest_rate_pct should come from tp_regulatory_rule for
    the relevant country/year; if not supplied, the corresponding exposure
    is left None with a clear note rather than assumed at some default."""
    actual = transaction.get("actual_result")
    base_amount = float(transaction.get("base_amount") or transaction.get("amount") or 0)
    stats = (benchmark or {}).get("stats") or {}
    arm_length = arm_length_result if arm_length_result is not None else stats.get("median")

    if actual is None or arm_length is None:
        return {"actual_result": actual, "arm_length_result": arm_length, "adjustment_amount": None,
                "taxable_income_impact": None, "tax_impact": None, "interest_exposure": None,
                "penalty_exposure": None, "total_exposure": None,
                "note": "Actual result or a computed arm's-length median is missing — nothing to compute yet."}

    diff_pct = (float(arm_length) - float(actual)) / 100
    adjustment_amount = R2(diff_pct * base_amount)
    taxable_income_impact = adjustment_amount

    tax_impact = R2(taxable_income_impact * float(tax_rate_pct) / 100) if tax_rate_pct is not None else None
    interest_exposure = (R2(tax_impact * float(interest_rate_pct) / 100 * periods)
                          if tax_impact is not None and interest_rate_pct is not None else None)
    penalty_exposure = R2(tax_impact * float(penalty_pct) / 100) if tax_impact is not None and penalty_pct is not None else None
    total_exposure = R2(sum(x for x in (tax_impact, interest_exposure, penalty_exposure) if x is not None)) \
        if any(x is not None for x in (tax_impact, interest_exposure, penalty_exposure)) else None

    notes = []
    if tax_rate_pct is None:
        notes.append("No tax rate supplied (add one to tp_regulatory_rule for this country/year) — tax impact not computed.")
    if interest_rate_pct is None:
        notes.append("No interest rate supplied — interest exposure not computed.")
    if penalty_pct is None:
        notes.append("No penalty rate supplied — penalty exposure not computed.")

    return {"actual_result": R2(actual), "arm_length_result": R2(arm_length), "adjustment_amount": adjustment_amount,
            "taxable_income_impact": R2(taxable_income_impact), "tax_impact": tax_impact,
            "interest_exposure": interest_exposure, "penalty_exposure": penalty_exposure,
            "total_exposure": total_exposure, "note": " ".join(notes) or None}


# --------------------------------------------------------- scenario analysis

def compute_scenarios(transaction, benchmark, tax_rate_pct=None, penalty_pct=None, interest_rate_pct=None,
                       periods=1, user_defined_arm_length=None):
    """Base Case / Low / Median / High / User-Defined exposure scenarios off
    the SAME benchmark set, so a preparer can see the exposure range rather
    than a single point estimate. Low/Median/High map onto the benchmark's
    Q1/median/Q3 — a common, defensible spread, not a statement that any one
    of them is what the tax authority would actually accept. Base Case
    assumes no adjustment at all (the taxpayer's filed position stands).
    User-Defined only appears when the caller supplies a figure."""
    stats = (benchmark or {}).get("stats") or {}
    actual = transaction.get("actual_result")
    scenarios = []

    scenarios.append({"scenario": "Base Case", "description": "No adjustment — the position as filed/actually priced stands.",
                       "arm_length_result": R2(actual) if actual is not None else None, "adjustment_amount": 0.0,
                       "taxable_income_impact": 0.0, "tax_impact": 0.0 if tax_rate_pct is not None else None,
                       "interest_exposure": None, "penalty_exposure": None,
                       "total_exposure": 0.0 if tax_rate_pct is not None else None,
                       "note": "By definition — no adjustment is being tested in this scenario."})

    for label, stat_key, desc in (
        ("Low Adjustment", "q1", "Arm's-length point taken at the benchmark set's lower quartile (Q1) — the most taxpayer-favourable defensible position within the range."),
        ("Median Adjustment", "median", "Arm's-length point taken at the benchmark set's median — the most commonly used single point estimate."),
        ("High Adjustment", "q3", "Arm's-length point taken at the benchmark set's upper quartile (Q3) — the most conservative (highest-exposure) defensible position within the range."),
    ):
        arm_length = stats.get(stat_key)
        result = compute_adjustment_exposure(transaction, benchmark, tax_rate_pct=tax_rate_pct, penalty_pct=penalty_pct,
                                              interest_rate_pct=interest_rate_pct, periods=periods, arm_length_result=arm_length)
        scenarios.append(dict(result, scenario=label, description=desc))

    if user_defined_arm_length is not None:
        result = compute_adjustment_exposure(transaction, benchmark, tax_rate_pct=tax_rate_pct, penalty_pct=penalty_pct,
                                              interest_rate_pct=interest_rate_pct, periods=periods,
                                              arm_length_result=user_defined_arm_length)
        scenarios.append(dict(result, scenario="User-Defined",
                               description="Arm's-length point set manually by the preparer, outside the benchmark quartiles above."))

    return {"scenarios": scenarios}
