"""Weekly and monthly candle reading, shared by closes.py (live) and the history test.
A candle is [t_open_unix, o, h, l, c]. Periods are UTC: weeks close Monday 00:00 UTC (07:00 Bangkok),
months close on the 1st at 00:00 UTC (07:00 Bangkok)."""
import datetime as dt

UTC = dt.timezone.utc
CATS = [  # key, score, label, plain explanation template
    ("new_high", 3.0, "NEW HIGH CLOSE", "closed above every close of the last 12 {p}s"),
    ("bull_engulf", 2.0, "BULLISH ENGULFING", "a green {p} that swallowed the whole red {p} before it"),
    ("bull_retest", 2.0, "BULLISH RETEST", "dipped back to its old high and held it as support"),
    ("bull_reject", 1.5, "BULLISH REJECTION", "broke under last {p}'s low, then closed near the top: buyers stepped in"),
    ("strong_bull", 1.5, "STRONG BULLISH CLOSE", "a big green {p} that closed above the last close"),
    ("bull_close", 1.0, "BULLISH CLOSE", "closed above last {p}'s close"),
    ("indecision", 0.0, "INDECISION", "tiny body or inside last {p}'s range: no side won"),
    ("bear_close", -1.0, "BEARISH CLOSE", "closed below last {p}'s close"),
    ("strong_bear", -1.5, "STRONG BEARISH CLOSE", "a big red {p} that closed below the last close"),
    ("bear_reject", -1.5, "BEARISH REJECTION", "broke above last {p}'s high, then closed near the bottom: sellers stepped in"),
    ("bear_retest", -2.0, "BEARISH RETEST", "bounced back to its old low and failed there"),
    ("bear_engulf", -2.0, "BEARISH ENGULFING", "a red {p} that swallowed the whole green {p} before it"),
    ("new_low", -3.0, "NEW LOW CLOSE", "closed below every close of the last 12 {p}s"),
]
CAT = {k: (s, l, e) for k, s, l, e in CATS}
BIG = {"week": 0.08, "month": 0.15}   # a "big" candle, as a share of price

def classify(cs, i, period):
    """Read candle cs[i] against what came before it. Needs at least 3 earlier candles."""
    if i < 0 or not cs: return None
    t, o, h, l, c = cs[i]
    if i == 0:   # first candle of a young index: nothing to compare with
        chg = c / o - 1 if o else 0.0
        return {"key": "first", "score": None, "label": "FIRST %s %s" % (period.upper(), "UP" if chg >= 0 else "DOWN"),
                "why": "its first %s, nothing earlier to compare with" % period, "chg": round(chg * 100, 1), "above_avg": None, "t": t}
    _, o1, h1, l1, c1 = cs[i - 1]
    prior = [x[4] for x in cs[max(0, i - 12):i]]
    older = [x[4] for x in cs[max(0, i - 13):i - 1]]
    rng = max(h - l, 1e-12); pos = (c - l) / rng; body = c - o
    chg = c / c1 - 1 if c1 else 0.0
    k = None
    if len(prior) >= 6 and c > max(prior): k = "new_high"
    elif len(prior) >= 6 and c < min(prior): k = "new_low"
    elif c > o and c1 < o1 and c >= o1 and o <= c1: k = "bull_engulf"
    elif c < o and c1 > o1 and c <= o1 and o >= c1: k = "bear_engulf"
    elif older and c > max(older) and l <= max(older) * 1.03 and c > o: k = "bull_retest"
    elif older and c < min(older) and h >= min(older) * 0.97 and c < o: k = "bear_retest"
    elif l < l1 and pos >= 0.65 and c >= c1: k = "bull_reject"
    elif h > h1 and pos <= 0.35 and c <= c1: k = "bear_reject"
    elif abs(body) <= 0.15 * rng or (h <= h1 and l >= l1 and abs(chg) < 0.02): k = "indecision"
    elif c > c1: k = "strong_bull" if chg >= BIG[period] else "bull_close"
    else: k = "strong_bear" if chg <= -BIG[period] else "bear_close"
    ma = sum(prior[-10:]) / len(prior[-10:]) if prior else None
    s, lab, ex = CAT[k]
    return {"key": k, "score": s, "label": lab, "why": ex.format(p=period), "chg": round(chg * 100, 1),
            "above_avg": (c > ma) if ma else None, "t": t}

def period_start(ts, period):
    d = dt.datetime.fromtimestamp(ts, UTC)
    if period == "week":
        d0 = (d - dt.timedelta(days=d.weekday())).replace(hour=0, minute=0, second=0, microsecond=0)
    else:
        d0 = d.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
    return int(d0.timestamp())

def next_boundary(now, period):
    d = dt.datetime.fromtimestamp(now, UTC)
    if period == "week":
        b = (d - dt.timedelta(days=d.weekday())).replace(hour=0, minute=0, second=0, microsecond=0) + dt.timedelta(days=7)
    else:
        b = (d.replace(day=1, hour=0, minute=0, second=0, microsecond=0) + dt.timedelta(days=32)).replace(day=1)
    return int(b.timestamp())

def prev_boundary(now, period):
    d = dt.datetime.fromtimestamp(now, UTC)
    if period == "week":
        b = (d - dt.timedelta(days=d.weekday())).replace(hour=0, minute=0, second=0, microsecond=0)
    else:
        b = d.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
    return int(b.timestamp())

def from_closes(points, period):
    """points: [[unix_ts_of_close, level], ...] sorted. A point stamped exactly on a boundary closes the period
    before it (CoinMarketCap daily snapshots are stamped 00:00 UTC = the close of the previous day).
    Returns candles [start, o, h, l, c] built from closes only (wicks are close to close)."""
    out = []; prev_c = None; cur = None
    for t, v in points:
        st = period_start(t - 1, period)      # 00:00 boundary point belongs to the period that just ended
        if cur is None or cur[0] != st:
            if cur is not None: out.append(cur); prev_c = cur[4]
            o = prev_c if prev_c is not None else v
            cur = [st, o, max(o, v), min(o, v), v]
        else:
            cur[2] = max(cur[2], v); cur[3] = min(cur[3], v); cur[4] = v
    if cur is not None: out.append(cur)
    return out

def overall(scores):
    scores = [x for x in scores if x is not None]
    if not scores: return {"label": "NO DATA", "tone": "grey", "score": None, "bull": 0, "bear": 0, "flat": 0, "n": 0}
    m = sum(scores) / len(scores)
    lab = ("VERY BULLISH" if m >= 1.5 else "BULLISH" if m >= 0.75 else "SLIGHTLY BULLISH" if m >= 0.25 else
           "NEUTRAL" if m > -0.25 else "SLIGHTLY BEARISH" if m > -0.75 else "BEARISH" if m > -1.5 else "VERY BEARISH")
    tone = "green" if m >= 0.75 else "lightgreen" if m >= 0.25 else "grey" if m > -0.25 else "lightred" if m > -0.75 else "red"
    return {"label": lab, "tone": tone, "score": round(m, 2), "bull": sum(1 for s in scores if s > 0),
            "bear": sum(1 for s in scores if s < 0), "flat": sum(1 for s in scores if s == 0), "n": len(scores)}
