"""Reproduce the page's synthetic geometry; Python 3 standard library only."""
import json
import math
from pathlib import Path

PHI = (1 + math.sqrt(5)) / 2
ROOT = Path(__file__).resolve().parent


def geometry(ta=0, pa=100, tb=10, pb=105, h=10, v=5, q=PHI, log=False):
    if not all(math.isfinite(z) for z in (ta, pa, tb, pb, h, v, q)):
        raise ValueError('Inputs must be finite')
    if tb <= ta or h <= 0 or v <= 0 or q <= 1:
        raise ValueError('Require B after A, positive units, and growth > 1')
    if log and (pa <= 0 or pb <= 0):
        raise ValueError('Log prices must be positive')
    # Log unit is ln(1.05), independent of the linear price unit v.
    unit = math.log(1.05) if log else v
    x = (tb - ta) / h
    y = math.log(pb / pa) / unit if log else (pb - pa) / unit
    radius, alpha, k = math.hypot(x, y), math.atan2(y, x), 2 * math.log(q) / math.pi

    def point(beta):
        r = radius * math.exp(k * (beta - alpha))
        yy = r * math.sin(beta)
        return ta + h * r * math.cos(beta), pa * math.exp(unit * yy) if log else pa + unit * yy

    return point, alpha


def target(t, **kwargs):
    point, _ = geometry(**kwargs)
    ta = kwargs.get('ta', 0)
    if not math.isfinite(t) or t < ta or t > point(2 * math.pi)[0]:
        return None
    lo, hi = 1.5 * math.pi, 2 * math.pi
    # The x coordinate is strictly increasing on this branch for q > 1.
    for _ in range(60):
        mid = (lo + hi) / 2
        if point(mid)[0] < t:
            lo = mid
        else:
            hi = mid
    return point((lo + hi) / 2)[1]


CASES = [
    ('Base: A(0,100), B(10,105)', {}),
    ('Move only A to bar 2', {'ta': 2}),
    ('Move only B price to 106', {'pb': 106}),
    ('Horizontal unit H = 20', {'h': 20}),
    ('Growth q = 1.5, not golden', {'q': 1.5}),
    ('Log price, unit ln(1.05)', {'log': True}),
]
EPISODES = [
    {'name': 'Rebound', 'low': 75, 'high': 76, 'close': 75.5, 'next_closes': [76.5, 78]},
    {'name': 'Failure before rebound', 'low': 75, 'high': 76, 'close': 75.5, 'next_closes': [73, 79]},
    {'name': 'No contact', 'low': 77, 'high': 78, 'close': 77.5, 'next_closes': [78, 80]},
    {'name': 'Expired', 'low': 75, 'high': 76, 'close': 75.5, 'next_closes': [76, 75.8]},
]


def score(episode):
    if not episode['low'] <= target(20) <= episode['high']:
        return 'No contact'
    for close in episode['next_closes']:
        if close <= episode['close'] - 2:
            return 'Failure'
        if close >= episode['close'] + 2:
            return 'Rebound'
    return 'Expired'


def svg_start(width, height, title):
    return [f'<svg xmlns="http://www.w3.org/2000/svg" width="{width}" height="{height}" viewBox="0 0 {width} {height}" role="img"><title>{title}</title><rect width="100%" height="100%" fill="#fff"/>', '<style>text{font-family:Arial,sans-serif;fill:#334155} .small{font-size:14px} .label{font-size:17px} .heading{font-size:23px;font-weight:600}</style>']


def line(parts, x1, y1, x2, y2, color='#cbd5e1', extra=''):
    parts.append(f'<line x1="{x1:.2f}" y1="{y1:.2f}" x2="{x2:.2f}" y2="{y2:.2f}" stroke="{color}" {extra}/>')


def path(parts, coords, color, width=2, extra=''):
    d = ' '.join(f'{"M" if i == 0 else "L"}{x:.3f},{y:.3f}' for i, (x, y) in enumerate(coords))
    parts.append(f'<path d="{d}" fill="none" stroke="{color}" stroke-width="{width}" {extra}/>')


def make_geometry():
    p = svg_start(1000, 550, 'Golden logarithmic spiral in fixed data coordinates')
    p.append('<text x="35" y="38" class="heading">One curve, one selected branch</text><text x="35" y="64" class="label">Synthetic anchors; the drawing is not a TradingView screenshot.</text>')
    sx = lambda t: 90 + (t + 50) / 140 * 820
    sy = lambda v: 440 - (v - 70) / 45 * 335
    for t in [-40, -20, 0, 20, 40, 60, 80]:
        line(p, sx(t), sy(70), sx(t), sy(115))
        p.append(f'<text x="{sx(t)-8}" y="465" class="small">{t}</text>')
    for v in [70, 80, 90, 100, 110]:
        line(p, sx(-50), sy(v), sx(90), sy(v))
        p.append(f'<text x="48" y="{sy(v)+5}" class="small">{v}</text>')
    point, alpha = geometry()
    # Negative angles show the inward continuation; A is the pole, not on the finite curve.
    path(p, [(sx(point(b)[0]), sy(point(b)[1])) for b in [alpha-6*math.pi+i*(8*math.pi-alpha)/1600 for i in range(1601)]], '#94a3b8', 2)
    path(p, [(sx(point(b)[0]), sy(point(b)[1])) for b in [1.5*math.pi+i*0.5*math.pi/300 for i in range(301)]], '#2563eb', 4)
    line(p, sx(20), sy(70), sx(20), sy(115), '#0f172a', 'stroke-dasharray="5 5"')
    for t,v,label,dy in [(0,100,'A: pole (0, 100)',-15),(10,105,'B: (10, 105)',-15),(20,target(20),'t = 20: 75.4481',27)]:
        p.append(f'<circle cx="{sx(t)}" cy="{sy(v)}" r="5" fill="#2563eb"/><text x="{sx(t)+10}" y="{sy(v)+dy}" class="label">{label}</text>')
    p.append('<text x="35" y="510" class="label">Blue: beta 270 to 360 degrees. Other branches can give other prices at the same bar.</text><text x="850" y="490" class="small">Bar t</text><text x="35" y="90" class="small">Price units</text></svg>')
    (ROOT/'geometry.svg').write_text(''.join(p))


def make_sensitivity():
    p = svg_start(1120, 710, 'Six frozen spiral specifications at the same bar')
    p.append('<text x="28" y="34" class="heading">A price of 76 changes sides when the specification changes</text><text x="28" y="60" class="label">Each panel: solid blue model, dashed gray baseline, vertical guide at bar 20.</text>')
    for i,(title,kw) in enumerate(CASES):
        col,row = i%3,i//3
        left,top = 55+col*365,123+row*290
        sx=lambda t:left+(t-10)/20*290
        sy=lambda v:top+190-(v-70)/16*190
        p.append(f'<text x="{left-16}" y="{top-34}" class="label">{title}</text><text x="{left-16}" y="{top-12}" class="label">P(20) = {target(20,**kw):.4f}</text>')
        for v in [70,76,86]:
            line(p,sx(10),sy(v),sx(30),sy(v))
            p.append(f'<text x="{left-28}" y="{sy(v)+4}" class="small">{v}</text>')
        for t in [10,20,30]:
            line(p,sx(t),sy(70),sx(t),sy(86),'#cbd5e1')
            p.append(f'<text x="{sx(t)-7}" y="{top+210}" class="small">{t}</text>')
        times=[10+j/10 for j in range(201)]
        path(p,[(sx(t),sy(target(t))) for t in times],'#64748b',2,'stroke-dasharray="5 4"')
        path(p,[(sx(t),sy(target(t,**kw))) for t in times],'#2563eb',3)
        p.append(f'<circle cx="{sx(20)}" cy="{sy(target(20,**kw))}" r="4" fill="#2563eb"/>')
    p.append('<text x="28" y="686" class="label">Synthetic calculations. All other inputs stay fixed within each comparison; units are bars and price.</text></svg>')
    (ROOT/'sensitivity.svg').write_text(''.join(p))


def verify():
    expected=[75.4481317487747,78.53177261223117,73.56709654745323,82.33758463947103,80.39464913151633,78.69599380260563]
    for (_,kw),value in zip(CASES,expected):
        assert math.isclose(target(20,**kw),value,abs_tol=1e-10)
        point,alpha=geometry(**kw)
        assert math.isclose(point(alpha)[0],kw.get('tb',10),abs_tol=1e-10)
        assert math.isclose(point(alpha)[1],kw.get('pb',105),abs_tol=1e-10)
    assert target(-1) is None and target(1000) is None
    for kwargs in [{'h':0},{'v':0},{'q':1},{'ta':10},{'log':True,'pa':0},{'q':float('inf')}]:
        try:
            target(20,**kwargs)
        except ValueError:
            pass
        else:
            raise AssertionError(f'Invalid inputs accepted: {kwargs}')
    assert [score(e) for e in EPISODES]==['Rebound','Failure','No contact','Expired']
    assert score({'low':target(20),'high':76,'close':75.5,'next_closes':[77.5,79]})=='Rebound'
    assert score({'low':75,'high':76,'close':75.5,'next_closes':[73.5,79]})=='Failure'


if __name__ == '__main__':
    verify()
    make_geometry()
    make_sensitivity()
    evidence={'kind':'synthetic arithmetic fixtures, not market observations','cases':[{'name':name,'parameters':kw,'price_at_bar_20':target(20,**kw)} for name,kw in CASES], 'episodes':[dict(e,outcome=score(e)) for e in EPISODES]}
    (ROOT/'calculations.json').write_text(json.dumps(evidence,indent=2)+'\n')
    print('PASS: 6 sensitivity cases, anchor recovery, branch bounds, invalid inputs, 4 outcomes, inclusive thresholds; 2 SVGs reproduced.')
