/* ============================================================================
   CA Automator — Transfer Pricing computation engines (browser mirror).

   Mirrors server/tp_engines.py line for line, same discipline as engines.js
   mirrors engines.py elsewhere in this app. Pure functions only — no DOM, no
   network. Every function here produces a *starting analysis* for a
   qualified professional to review, never a final legal or economic
   conclusion; that framing lives in the returned basis/explanation text.

   Nothing here embeds a country's substantive law — country-specific
   figures come from the cached tp_regulatory_rule rows via the caller.
   ========================================================================= */
(function (global) {
  'use strict';

  const R2 = x => Math.round((Number(x) || 0) * 100) / 100;

  // ------------------------------------------------------------- benchmarking

  function percentile(sortedVals, pct) {
    if (sortedVals.length === 1) return sortedVals[0];
    const k = (sortedVals.length - 1) * pct;
    const f = Math.floor(k), c = Math.min(Math.floor(k) + 1, sortedVals.length - 1);
    if (f === c) return sortedVals[f];
    return sortedVals[f] + (sortedVals[c] - sortedVals[f]) * (k - f);
  }

  function weightedPercentile(sortedValues, weights, pct) {
    const pairs = sortedValues.map((v, i) => [v, weights[i]]).sort((a, b) => a[0] - b[0]);
    const total = pairs.reduce((s, p) => s + p[1], 0);
    if (total <= 0) return null;
    let cum = 0;
    const target = pct * total;
    for (const [v, w] of pairs) {
      cum += w;
      if (cum >= target) return v;
    }
    return pairs[pairs.length - 1][0];
  }

  function computeBenchmarkStats(comparables) {
    const values = (comparables || []).filter(c => c.value !== null && c.value !== undefined)
      .map(c => Number(c.value)).sort((a, b) => a - b);
    const n = values.length;
    if (n === 0) {
      return { count: 0, min: null, max: null, mean: null, median: null, q1: null, q3: null,
               iqr: null, weighted_average: null, weighted_median: null, adjusted_range: null };
    }
    const mean = values.reduce((s, v) => s + v, 0) / n;
    const median = percentile(values, 0.5);
    const q1 = percentile(values, 0.25), q3 = percentile(values, 0.75);
    const iqr = q3 - q1;

    const weights = (comparables || []).filter(c => c.value !== null && c.value !== undefined)
      .map(c => Number(c.weight || 1));
    const totalW = weights.reduce((s, w) => s + w, 0);
    const weightedAverage = totalW ? values.reduce((s, v, i) => s + v * weights[i], 0) / totalW : null;
    const weightedMedian = totalW ? weightedPercentile(values, weights, 0.5) : null;

    let adjustedRange = null;
    if (n >= 5) {
      const trim = Math.max(1, Math.round(n * 0.1));
      const trimmed = (n - 2 * trim >= 1) ? values.slice(trim, n - trim) : values;
      adjustedRange = { min: R2(trimmed[0]), max: R2(trimmed[trimmed.length - 1]) };
    }

    return {
      count: n, min: R2(values[0]), max: R2(values[n - 1]), mean: R2(mean), median: R2(median),
      q1: R2(q1), q3: R2(q3), iqr: R2(iqr),
      weighted_average: weightedAverage !== null ? R2(weightedAverage) : null,
      weighted_median: weightedMedian !== null ? R2(weightedMedian) : null,
      adjusted_range: adjustedRange
    };
  }

  function workingCapitalAdjustment(comparable, testedParty, financingRatePct) {
    const nwcRatio = d => {
      const sales = Number(d.sales) || 0;
      if (!sales) return null;
      const nwc = (Number(d.receivables) || 0) + (Number(d.inventory) || 0) - (Number(d.payables) || 0);
      return nwc / sales;
    };
    const tpRatio = nwcRatio(testedParty), compRatio = nwcRatio(comparable);
    if (tpRatio === null || compRatio === null) {
      return { adjustment_pct: null, tested_party_nwc_ratio: null, comparable_nwc_ratio: null,
               note: 'Sales figure missing for the tested party or the comparable — cannot compute a ratio.' };
    }
    const rate = (Number(financingRatePct) || 0) / 100;
    const adjustmentPct = rate * (tpRatio - compRatio);
    return {
      adjustment_pct: R2(adjustmentPct * 100), tested_party_nwc_ratio: R2(tpRatio * 100),
      comparable_nwc_ratio: R2(compRatio * 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

  const 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'
  };

  function recommendMethod(transaction, far, comparablesAvailable) {
    const cat = (transaction.transaction_category || '').toLowerCase();
    const profile = (far.functional_profile || '').toLowerCase();
    const ca = comparablesAvailable || {};

    if (ca.internal_cup) {
      return { recommended_method: 'cup', label: METHOD_LABELS.cup,
        basis: 'An internal comparable uncontrolled transaction is available — CUP is generally the most ' +
               'direct method when a reliable internal or external comparable price exists.' };
    }
    if (cat === 'financing' || cat === 'loan' || cat === 'guarantee') {
      return { recommended_method: 'cup', label: METHOD_LABELS.cup,
        basis: 'Financing/guarantee arrangements are typically benchmarked against comparable uncontrolled ' +
               'interest rates or fee rates (a CUP-style approach), subject to data availability.' };
    }
    if (ca.unique_intangible || (cat === 'royalty' && (profile === 'ip_owner' || profile === 'entrepreneurial'))) {
      return { recommended_method: 'psm', label: METHOD_LABELS.psm,
        basis: '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.' };
    }
    if ((profile === 'routine_distributor' || profile === 'limited_risk_distributor') && cat === 'goods') {
      if (ca.external_gross_margin) {
        return { recommended_method: 'rpm', label: METHOD_LABELS.rpm,
          basis: 'A limited-risk distributor reselling goods, with external gross-margin comparables ' +
                 'available, is a classic Resale Price Method fact pattern.' };
      }
      return { recommended_method: 'tnmm', label: METHOD_LABELS.tnmm,
        basis: '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.' };
    }
    if (profile === 'contract_manufacturer' && ca.external_gross_margin) {
      return { recommended_method: 'cpm', label: METHOD_LABELS.cpm,
        basis: '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: 'tnmm', label: METHOD_LABELS.tnmm,
      basis: "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.' };
  }

  // --------------------------------------------------------------- risk scoring

  function scoreRisk(transaction, far, benchmark, flags) {
    flags = flags || {};
    const factors = [];
    const add = (name, hit, points, note) => { if (hit) factors.push({ factor: name, points, note }); };

    const amount = Number(transaction.amount) || 0;
    add('Large transaction value', amount >= 10000000, 2, 'Transaction value is NPR/local-currency-equivalent 1 crore or more (illustrative threshold, not a statutory one).');
    add('Moderate transaction value', amount >= 1000000 && amount < 10000000, 1, 'Transaction value is material but below the large-transaction band.');

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

    add('Loss-making tested party', !!flags.loss_making, 2, 'The tested party reported a loss for the period.');
    add('Persistent losses', !!flags.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', !!flags.related_party_pct_high, 1, "Related-party transactions form a large share of the entity's total transactions.");
    add('Cross-border transaction', !!flags.cross_border, 1, "Source and destination countries differ, adding a second jurisdiction's rules to consider.");
    add('Historically scrutinised category', !!flags.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', !!flags.documentation_gap || !far.functional_profile, 2, 'FAR analysis or supporting documentation is incomplete for this transaction.');

    const total = factors.reduce((s, f) => s + f.points, 0);
    let level;
    if (total >= 8) level = 'Very High';
    else if (total >= 5) level = 'High';
    else if (total >= 2) level = 'Medium';
    else level = 'Low';

    return { risk_level: level, score: total, factors,
      explanation: `${level} risk — ${factors.length} 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

  function computeAdjustmentExposure(transaction, benchmark, taxRatePct, penaltyPct, interestRatePct, periods, armLengthResult) {
    periods = periods || 1;
    const actual = transaction.actual_result;
    const baseAmount = Number(transaction.base_amount || transaction.amount) || 0;
    const stats = ((benchmark || {}).stats) || {};
    const armLength = (armLengthResult !== null && armLengthResult !== undefined) ? armLengthResult : stats.median;

    if (actual === null || actual === undefined || armLength === null || armLength === undefined) {
      return { actual_result: actual, arm_length_result: armLength, adjustment_amount: null,
               taxable_income_impact: null, tax_impact: null, interest_exposure: null,
               penalty_exposure: null, total_exposure: null,
               note: "Actual result or a computed arm's-length median is missing — nothing to compute yet." };
    }

    const diffPct = (Number(armLength) - Number(actual)) / 100;
    const adjustmentAmount = R2(diffPct * baseAmount);
    const taxableIncomeImpact = adjustmentAmount;

    const taxImpact = (taxRatePct !== null && taxRatePct !== undefined) ? R2(taxableIncomeImpact * Number(taxRatePct) / 100) : null;
    const interestExposure = (taxImpact !== null && interestRatePct !== null && interestRatePct !== undefined)
      ? R2(taxImpact * Number(interestRatePct) / 100 * periods) : null;
    const penaltyExposure = (taxImpact !== null && penaltyPct !== null && penaltyPct !== undefined)
      ? R2(taxImpact * Number(penaltyPct) / 100) : null;
    const parts = [taxImpact, interestExposure, penaltyExposure].filter(x => x !== null);
    const totalExposure = parts.length ? R2(parts.reduce((s, x) => s + x, 0)) : null;

    const notes = [];
    if (taxRatePct === null || taxRatePct === undefined) notes.push('No tax rate supplied (add one to tp_regulatory_rule for this country/year) — tax impact not computed.');
    if (interestRatePct === null || interestRatePct === undefined) notes.push('No interest rate supplied — interest exposure not computed.');
    if (penaltyPct === null || penaltyPct === undefined) notes.push('No penalty rate supplied — penalty exposure not computed.');

    return { actual_result: R2(actual), arm_length_result: R2(armLength), adjustment_amount: adjustmentAmount,
             taxable_income_impact: R2(taxableIncomeImpact), tax_impact: taxImpact,
             interest_exposure: interestExposure, penalty_exposure: penaltyExposure,
             total_exposure: totalExposure, note: notes.join(' ') || null };
  }

  // --------------------------------------------------------- scenario analysis

  function computeScenarios(transaction, benchmark, taxRatePct, penaltyPct, interestRatePct, periods, userDefinedArmLength) {
    const stats = ((benchmark || {}).stats) || {};
    const actual = transaction.actual_result;
    const scenarios = [];

    scenarios.push({ scenario: 'Base Case', description: 'No adjustment — the position as filed/actually priced stands.',
      arm_length_result: (actual !== null && actual !== undefined) ? R2(actual) : null, adjustment_amount: 0,
      taxable_income_impact: 0, tax_impact: (taxRatePct !== null && taxRatePct !== undefined) ? 0 : null,
      interest_exposure: null, penalty_exposure: null,
      total_exposure: (taxRatePct !== null && taxRatePct !== undefined) ? 0 : null,
      note: 'By definition — no adjustment is being tested in this scenario.' });

    [
      ['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."]
    ].forEach(([label, statKey, desc]) => {
      const armLength = stats[statKey];
      const result = computeAdjustmentExposure(transaction, benchmark, taxRatePct, penaltyPct, interestRatePct, periods, armLength);
      scenarios.push(Object.assign({}, result, { scenario: label, description: desc }));
    });

    if (userDefinedArmLength !== null && userDefinedArmLength !== undefined) {
      const result = computeAdjustmentExposure(transaction, benchmark, taxRatePct, penaltyPct, interestRatePct, periods, userDefinedArmLength);
      scenarios.push(Object.assign({}, result, { scenario: 'User-Defined',
        description: "Arm's-length point set manually by the preparer, outside the benchmark quartiles above." }));
    }

    return { scenarios };
  }

  global.TPEngines = {
    computeBenchmarkStats, workingCapitalAdjustment, recommendMethod, scoreRisk, computeAdjustmentExposure,
    computeScenarios, METHOD_LABELS, R2
  };
})(window);
