"""Reproduce the figures: python generate-payout-charts.py (requires matplotlib).
Reads creator-payout-rules-data.csv alongside this script. No transaction data.
"""
from pathlib import Path
import csv
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from matplotlib.ticker import MultipleLocator, StrMethodFormatter

BASE = Path(__file__).resolve().parent
DATA = list(csv.DictReader((BASE / 'creator-payout-rules-data.csv').open()))
INK, BAR, EXCEPTION, PAPER = '#fff8eb', '#d7a956', '#c3cbd5', '#0e1117'
plt.rcParams.update({'font.family': 'DejaVu Sans', 'font.size': 13, 'text.parse_math': False,
 'text.color': INK, 'axes.labelcolor': INK, 'xtick.color': INK,
 'ytick.color': INK, 'svg.fonttype': 'none', 'figure.facecolor': PAPER,
 'axes.facecolor': PAPER, 'savefig.facecolor': PAPER})

def record(platform, field, method=None):
 rows = [r for r in DATA if r['platform']==platform and r['field']==field
         and (method is None or r['method_or_scope']==method)]
 assert len(rows)==1, (platform, field, method)
 return rows[0]

def number(row):
 return float(row['published_value'])

def style(ax):
 ax.set_axisbelow(True)
 ax.grid(axis='x', color='#2c3440', linewidth=.8)
 for edge in ax.spines.values(): edge.set_visible(False)
 ax.tick_params(axis='both', length=0, pad=10)

def save(fig, name, description):
 for ext in ['png','svg']:
  metadata = {'Description': description} if ext=='svg' else {'Description':description}
  fig.savefig(BASE/f'{name}.{ext}', dpi=180, metadata=metadata)
 plt.close(fig)

entries = [
 ('Fansly · local / wallet routes', record('Fansly','minimum','Paxum / Cosmopayment')),
 ('Fansly · SEPA standard*', record('Fansly','standard threshold')),
 ('Fansly · SWIFT / crypto', record('Fansly','minimum','SWIFT / cryptocurrency')),
 ('Fanvue · regional baseline†', record('Fanvue','minimum')),
 ('FeetFinder · regional routes', record('FeetFinder','minimum')),
 ('Footly · ACH / Paxum', record('Footly','minimum')),
]
fig, ax = plt.subplots(figsize=(13,8.2))
fig.subplots_adjust(left=.34,right=.94,bottom=.30,top=.77)
fig.text(.055,.94,'WITHDRAWAL REQUIREMENTS / FIGURE 1',size=11,weight='bold',color=BAR)
fig.text(.055,.878,'The payout method changes the threshold',size=23,weight='bold')
fig.text(.055,.824,'Published policy amounts in USD • reviewed September 29, 2026',size=13)
values=[number(r) for _,r in entries]
ax.barh(range(len(entries)),values,color=BAR,height=.48)
ax.set_yticks(range(len(entries)),[label for label,_ in entries])
ax.invert_yaxis();ax.set_xlim(0,115)
ax.xaxis.set_major_locator(MultipleLocator(20));ax.xaxis.set_major_formatter(StrMethodFormatter('${x:.0f}'))
ax.set_xlabel('Eligible balance threshold (USD)',labelpad=12)
for i,v in enumerate(values):
 ax.text(v+2,i,f'${v:g}'+(' baseline' if i==3 else ''),va='center',weight='bold',size=12)
style(ax)
fig.text(.055,.18,'* SEPA: requests from $20; the minimums guide describes a 14-day wait if totals stay below $50.',size=11)
fig.text(.055,.145,'† Fanvue: regional minimums may exceed $20; this chart does not establish an upper limit.',size=11)
fig.text(.055,.11,'Available routes vary by country. Thresholds are not sales commissions, costs or payout-speed scores.',size=11)
fig.text(.055,.055,'Sources: official Footly / FeetFinder guides and Fansly / Fanvue help centres. Full URLs in companion CSV.',size=10,color='#a8b0b9')
fig.text(.055,.024,'Footly policy audit • publisher operates one of the platforms • no transaction testing',size=10,color='#a8b0b9')
save(fig,'payout-thresholds-dark','Published withdrawal thresholds by platform and method, with SEPA and regional qualifications.')

holds=[('Fansly',record('Fansly','earnings hold')),('Fanvue',record('Fanvue','earnings hold')),
 ('FeetFinder',record('FeetFinder','earnings review')),('Footly',record('Footly','earnings hold'))]
fig,ax=plt.subplots(figsize=(13,7.5))
fig.subplots_adjust(left=.18,right=.94,bottom=.31,top=.73)
fig.text(.055,.94,'EARNINGS AVAILABILITY / FIGURE 2',size=11,weight='bold',color=BAR)
fig.text(.055,.872,'The earnings hold comes before the transfer',size=23,weight='bold')
fig.text(.055,.815,'Published hold or review periods • not observed sale-to-bank delivery times',size=13)
for i,(platform,r) in enumerate(holds):
 value=number(r)
 ax.hlines(i,0,value,color=BAR,linewidth=8)
 ax.scatter([value],[i],s=85,color=BAR,zorder=4)
 ax.text(value+.65,i-.13,f'{value:g} days'+(' typical' if platform=='Fanvue' else ''),size=12,weight='bold')
 if platform=='Fanvue':
  # The exception is explicit in the audited CSV qualification, not a percentile.
  assert '30 days' in r['qualification']
  ax.scatter([30],[i],s=130,edgecolor=EXCEPTION,facecolor=PAPER,linewidth=2,zorder=4)
  ax.text(30,i+.43,'30-day email-verification exception',ha='right',size=11,color=EXCEPTION)
ax.set_yticks(range(4),[p for p,_ in holds]);ax.set_ylim(3.6,-.6);ax.set_xlim(0,35)
ax.xaxis.set_major_locator(MultipleLocator(7));ax.set_xlabel('Published duration (days)',labelpad=12);style(ax)
fig.text(.055,.19,'Filled marks: stated hold / review. Open mark: Fanvue’s unverified-fan-email exception.',size=11)
fig.text(.055,.15,'Starting events and terminology differ. Review, reserve, schedule and transfer delays can also apply.',size=11)
fig.text(.055,.11,'No averages, delivery guarantees or statistically measured ranges are shown.',size=11)
fig.text(.055,.055,'Sources: Footly payout guide; Fansly processing guide; Fanvue balance guide; FeetFinder Terms of Service.',size=10,color='#a8b0b9')
fig.text(.055,.024,'Footly policy audit • September 29, 2026 • full source URLs and qualifications in companion CSV',size=10,color='#a8b0b9')
save(fig,'earnings-hold-periods-dark','Published earnings hold or review durations: Fansly 7, Fanvue typically 7 with a 30-day exception, FeetFinder 30, Footly 7 days.')
print('Generated two reproducible PNG/SVG chart pairs from the source CSV.')
