"""BorrowYourLoan Loan Cost Index v1.0.0; illustrative fee-free math, not offers.
Run: python reproduce-v1.py OUTPUT_DIRECTORY
Python 3 standard library only. All outputs are deterministic, including dates.
"""
from decimal import Decimal,localcontext,ROUND_HALF_UP
from pathlib import Path
import csv,json,sys
VERSION='1.0.0';DATE='2026-10-22'
AMOUNTS=[2500,5000,7500,10000,15000,20000,25000,30000,40000,50000]
RATES=[6,10,15,20,25,30,36];TERMS=[12,24,36,48,60,72]
CANONICAL='https://www.borrowyourloan.com/resources/loan-cost-index'
def calculate(amount,rate,months):
    with localcontext() as c:
        c.prec=60;p=Decimal(amount);r=Decimal(rate)/1200
        payment=p/months if not r else p*r/(1-(1+r)**(-months))
        total=payment*months
        return payment,total-p,total
def cents(value):return format(value.quantize(Decimal('.01'),rounding=ROUND_HALF_UP),'.2f')
def build():
    scenarios=[];comparisons=[]
    for amount in AMOUNTS:
        for rate in RATES:
            for term in TERMS:
                values=calculate(amount,rate,term);baseline_rate=calculate(amount,10,term);baseline_term=calculate(amount,rate,36)
                row={'scenario_id':f'P{amount}-R{rate}-T{term}','principal_usd':str(amount),'annual_interest_rate_pct':str(rate),'illustrative_fee_free_apr_pct':str(rate),'term_months':term}
                for k,v in zip(['monthly_payment_usd','total_interest_usd','total_repayment_usd'],values):row[k]=cents(v)
                for prefix,baseline in [('versus_10pct_',baseline_rate),('versus_36months_',baseline_term)]:
                    for k,v,b in zip(['payment_delta_usd','interest_delta_usd','repayment_delta_usd'],values,baseline):row[prefix+k]=cents(v-b)
                scenarios.append(row)
            for short,long in [(24,36),(36,60)]:
                a=calculate(amount,rate,short);b=calculate(amount,rate,long)
                row={'principal_usd':str(amount),'annual_interest_rate_pct':str(rate),'short_term_months':short,'long_term_months':long}
                for prefix,values in [('short_',a),('long_',b)]:
                    for key,value in zip(['payment_usd','interest_usd','repayment_usd'],values):row[prefix+key]=cents(value)
                row['monthly_payment_reduction_usd']=cents(a[0]-b[0]);row['additional_interest_usd']=cents(b[1]-a[1]);row['additional_repayment_usd']=cents(b[2]-a[2]);comparisons.append(row)
    return scenarios,comparisons
def write_csv(path,rows):
    with path.open('w',newline='',encoding='utf-8') as f:
        w=csv.DictWriter(f,fieldnames=list(rows[0]),lineterminator='\n');w.writeheader();w.writerows(rows)
def generate(folder):
    folder=Path(folder);folder.mkdir(parents=True,exist_ok=True);rows,pairs=build()
    assumptions=['Synthetic mathematical scenarios; not observed rates, market averages, lender quotes or available loan offers.','Fixed annual interest rate equals APR only because modeled mandatory fees are zero.','Equal monthly periods, first payment one month after disbursement, fully amortizing scheduled payments, no missed/extra payments.','No origination fees, taxes, insurance, penalties, optional products, daily-interest or irregular-period adjustments.','Scenario coverage does not establish product or term availability, state eligibility or underwriting criteria.','60-digit Decimal calculations; ROUND_HALF_UP to cents only for output; totals and deltas use unrounded values.']
    metadata={'title':'BorrowYourLoan Loan Cost Index — Personal Loan Scenarios','version':VERSION,'date_created':DATE,'responsible_organization':'Borrow Your Loan','canonical_url':CANONICAL,'methodology_url':CANONICAL+'-methodology','type':'Deterministic scenario dataset, not a market-rate index','scenario_count':len(rows),'comparison_count':len(pairs),'amounts_usd':AMOUNTS,'rates_pct':RATES,'terms_months':TERMS,'assumptions':assumptions,'formula':'r = annual_interest_rate_pct / 1200; M = P*r/(1-(1+r)^(-n)); at r=0, M=P/n; total=M*n; interest=total-P','rounding':'ROUND_HALF_UP to cents after full-precision calculation','definitions':{'scenario_id':'Unique principal/rate/term identifier.','principal_usd':'Gross modeled principal in US dollars.','annual_interest_rate_pct':'Fixed nominal annual interest rate; divide by 1200 to obtain monthly rate.','illustrative_fee_free_apr_pct':'Equals modeled interest rate only under zero-fee assumptions. Not a regulatory APR disclosure.','term_months':'Number of monthly payments.','monthly_payment_usd':'Unrounded amortization payment rounded to cents for display.','total_interest_usd':'Unrounded total repayment less principal, rounded to cents.','total_repayment_usd':'Unrounded monthly payment multiplied by term, rounded to cents.','versus_10pct_*':'Scenario minus 10% scenario at identical principal and term, computed before rounding. Positive means higher.','versus_36months_*':'Scenario minus 36-month scenario at identical principal and rate, computed before rounding. Positive means higher.','short_* / long_*':'Payment, interest and repayment for the named terms at identical principal/rate.','monthly_payment_reduction_usd':'Short-term payment minus long-term payment, rounded only after subtraction.','additional_interest_usd / additional_repayment_usd':'Long-term outcome minus short-term outcome; no fees in this model.'},'interpretation':'This is a scenario reference grid, not a weighted economic index or prediction. $30,000–$50,000 examples and 12/72-month examples are stress/comparison inputs, not BYL availability claims.','primary_reference':'https://www.consumerfinance.gov/ask-cfpb/what-is-the-difference-between-a-loan-interest-rate-and-the-apr-en-733/'}
    write_csv(folder/'personal-loan-cost-v1.csv',rows);write_csv(folder/'10000-loan-cost-v1.csv',[r for r in rows if r['principal_usd']=='10000']);write_csv(folder/'term-comparisons-v1.csv',pairs)
    (folder/'personal-loan-cost-v1.json').write_text(json.dumps({'metadata':metadata,'scenarios':rows,'term_comparisons':pairs},indent=2)+'\n',encoding='utf-8')
    (folder/'data-dictionary-v1.json').write_text(json.dumps(metadata,indent=2)+'\n',encoding='utf-8')
    return len(rows),len(pairs)
if __name__=='__main__':
    count,pairs=generate(sys.argv[1] if len(sys.argv)>1 else '.');print(f'Generated {count} scenarios and {pairs} term comparisons; version {VERSION}.')
