"""The 24/7 loop. Run this on a server (VPS), not on your laptop."""
import json
import logging
import time
import traceback
from .backtest import split_backtest
from .broker import NeedAccount, SimBroker, make_broker
from .config import STATE, REPORTS
from .data import fetch_candles, fetch_history, get_price, public_exchange
from .engine import Engine
from .evolve import autopsy, check_failure
from .gate import evaluate
from .journal import Journal
from .metrics import summarize
from .report import save_and_send
from .brain import make_brain
from .risk import Risk
from .util import utcnow

log = logging.getLogger("sailar")
_BRAIN = None    # the AI analyst (None until run() starts)


def _price(broker, ex, cfg):
    if isinstance(broker, SimBroker):
        return get_price(cfg, ex)
    return broker.price(cfg["symbol"])


def _backtest_version(cfg, j, version, params, ex):
    try:
        df = fetch_history(cfg, days=60, ex=ex)
        res, _ = split_backtest(df, params, cfg)
        j.set_backtest(version, res)
        j.event("INFO", f"v{version} backtest (unseen part): {json.dumps(res['unseen_test'])}")
    except Exception as e:
        j.event("WARN", f"v{version} could not be backtested: {e}")


def start_version(cfg, j, params, ex, parent=None, findings=None):
    n = len(j.all_versions()) + 1
    v = j.new_version(params, cfg["sim_start_balance"], parent, account=n, findings=findings)
    _backtest_version(cfg, j, v, params, ex)
    return j.version(v)


def attach(cfg, j, row):
    broker = make_broker(cfg, row)
    if not j.equity_series(row["version"]):
        j.set_start_equity(row["version"], broker.equity() if not isinstance(broker, SimBroker) else broker.cash)
    eng = Engine(cfg, broker, j, Risk(cfg), row["version"], row["params"])
    eng.brain = _BRAIN
    return broker, eng


def write_status(cfg, j, row, eng, broker, state, note=""):
    v = row["version"]
    sig = eng.last_signal.to_dict() if eng and eng.last_signal else None
    eq = None
    try:
        eq = broker.equity() if broker else None
    except Exception:
        pass
    trades = j.trades(v)
    gate = evaluate(j, v, cfg)

    # equity curve – last 100 sampled points for sparkline
    eq_series = j.equity_series(v)
    step = max(1, len(eq_series) // 100)
    eq_curve = eq_series[::step][-100:]

    # session (today) stats
    now = utcnow()
    today_str = str(now.date())
    today_cl = [
        t for t in trades
        if t.get("status") == "closed" and str(t.get("exit_time", ""))[:10] == today_str
    ]
    session = {
        "trades": len(today_cl),
        "pnl": round(sum(t["pnl"] for t in today_cl), 2),
        "wins": sum(1 for t in today_cl if t["pnl"] > 0),
    }

    full_stats = summarize(trades, row["start_equity"], eq_series)
    st = {
        "updated": str(now), "state": state, "note": note, "mode": cfg["mode"],
        "symbol": cfg["symbol"], "version": v, "params": row["params"],
        "equity": eq, "start_equity": row["start_equity"],
        "drawdown_pct": eng.drawdown_pct() if eng else 0,
        "signal": sig, "position": j.current_trade(v),
        "recent_trades": trades[-20:][::-1],
        "stats": full_stats,
        "session": session,
        "equity_curve": eq_curve,
        "gate": gate, "events": j.events(20), "versions": j.all_versions(),
        "brain": _BRAIN.snapshot() if _BRAIN else None,
    }
    tmp = STATE / "status.json.tmp"
    tmp.write_text(json.dumps(st, default=str))
    tmp.replace(STATE / "status.json")


def run(cfg):
    logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s",
                        handlers=[logging.StreamHandler(), logging.FileHandler(STATE / "sailar.log")])
    j = Journal(STATE / "journal.db")
    ex = public_exchange(cfg)
    global _BRAIN
    _BRAIN = make_brain(cfg, j)
    import os
    search_prov = "serper" if os.environ.get("SERPER_API_KEY") else ("tavily" if os.environ.get("TAVILY_API_KEY") else ("gemini" if _BRAIN.router.has("gemini") else "none"))
    log.info("AI brain: mode=%s, providers=%s, search=%s", _BRAIN.mode, _BRAIN.router.order or "none", search_prov)
    row = j.active_version() or start_version(cfg, j, cfg["strategy"], ex)
    broker = eng = None
    report_day = None
    log.info("Sailar-x starting: mode=%s v%s", cfg["mode"], row["version"])

    while True:
        now = utcnow()
        try:
            if broker is None:
                broker, eng = attach(cfg, j, row)
            if Risk.killed():
                msg = eng.close_all("KILL", now)
                j.event("WARN", f"KILL switch: {msg}")
                write_status(cfg, j, row, eng, broker, "killed")
                log.warning("Kill switch is on. Stopping.")
                return
            df = fetch_candles(cfg, ex)
            for m in eng.step(df, now, price=_price(broker, ex, cfg)):
                j.event("TRADE", m)
                log.info(m)
            if _BRAIN:
                _BRAIN.tick(now)          # keeps the web research fresh in the background
            v = row["version"]
            j.log_equity(v, broker.equity(), now)
            if isinstance(broker, SimBroker):
                broker.save(broker.state_path)

            why = check_failure(cfg, j.trades(v, "closed"), eng.drawdown_pct())
            if why:
                j.event("WARN", f"v{v} FAILED: {why}")
                log.warning("v%s failed: %s", v, why)
                eng.close_all("VERSION_FAILED", now)
                trades = j.trades(v)
                j.retire(v, why, now)
                if len(j.all_versions()) >= cfg["failure"]["max_versions"]:
                    write_status(cfg, j, row, eng, broker, "stopped", "max versions reached: needs a human")
                    log.error("Max versions reached. Stopping for a human review.")
                    return
                notes, new_params = autopsy(trades, row["params"])
                if _BRAIN:
                    concl = _BRAIN.failure_conclusion(v, summarize(trades, row["start_equity"], j.equity_series(v)), row["params"], notes)
                    if concl:
                        notes.append("AI conclusion (unverified, test before trusting): " + concl)
                for n in notes:
                    j.event("INFO", f"autopsy: {n}")
                row = start_version(cfg, j, new_params, ex, parent=v, findings=notes)
                broker = eng = None  # next loop attaches a fresh account for the new version
                continue

            today = str(now.date())
            if today != report_day:
                if report_day is not None:
                    rp = save_and_send(j, v, cfg, broker.equity())
                    if _BRAIN:
                        note = _BRAIN.daily_note(v, summarize(j.trades(v, "closed"), row["start_equity"], j.equity_series(v)), {})
                        if note:
                            rp.write_text(rp.read_text() + "\n\n## AI analyst note (unverified)\n" + note + "\n")
                report_day = today
            write_status(cfg, j, row, eng, broker, "running")
        except NeedAccount as e:
            write_status(cfg, j, row, eng, broker, "waiting_for_account", str(e))
            log.warning(str(e))
            time.sleep(60)
            continue
        except KeyboardInterrupt:
            log.info("Stopped by user")
            return
        except Exception:
            j.event("CRASH", traceback.format_exc()[-600:])
            log.exception("loop error")
            time.sleep(min(300, cfg["poll_seconds"] * 3))
            continue
        time.sleep(cfg["poll_seconds"])
