"""
═══════════════════════════════════════════════════════════════════════════════
  reset_account.py — Dual FX Macro Model state reset utility
═══════════════════════════════════════════════════════════════════════════════

  Purpose
  -------
  Cleanly resets the monitor's persistent state for a fresh trading account
  (e.g., a new FTMO challenge attempt). Archives all current state before
  overwriting so nothing is ever truly lost.

  Usage
  -----
      # Interactive (default, recommended) — prompts before any changes
      python reset_account.py

      # Preview what would happen without making changes
      python reset_account.py --dry-run

      # Skip confirmation prompts (scripted use — dangerous, avoid)
      python reset_account.py --force

      # Combine: preview + skip prompts
      python reset_account.py --dry-run --force

  What gets RESET
  ---------------
    • data/logs/trade_history_eurusd.json    → []
    • data/logs/trade_history_usdjpy.json    → []
    • data/logs/state_eurusd.json            → blank template
    • data/logs/state_usdjpy.json            → blank template
    • data/logs/account_snapshot.json        → deleted (regenerates)
    • data/logs/live_monitor_v2.log          → moved to archive, new empty file
    • dashboard_data.json                    → deleted (auto-regenerates)

  What gets PRESERVED
  -------------------
    • data/logs/telegram_recipients.json     ← friend list stays intact
    • data/raw/rates/*.csv                   ← 23yr of market data, untouched
    • Source code (unless STARTING_BALANCE changed via prompt)
    • All configuration files

  Safety guarantees
  -----------------
    1. Monitor process detection — refuses to run if monitor appears active
    2. Typed confirmation — user must type "RESET" exactly
    3. Archive-first — every file copied to timestamped archive folder BEFORE
       modification; full rollback possible by copying archive back
    4. Audit trail — every action written to manifest.txt in archive folder
    5. Dry-run mode — simulate the entire process without touching anything
    6. Verification pass — after reset, re-reads every file to confirm
       correct state

  Author: Built for Bennett's Dual FX Macro Model project
═══════════════════════════════════════════════════════════════════════════════
"""

from __future__ import annotations
import argparse
import json
import os
import re
import shutil
import subprocess
import sys
import time
from dataclasses import dataclass, field
from datetime import datetime
from pathlib import Path


# ═══════════════════════════════════════════════════════════════════════════
#  PATH DISCOVERY
# ═══════════════════════════════════════════════════════════════════════════
# This script lives at <project>/src/execution/reset_account.py
# Project root is two directories up from this file.
SCRIPT_PATH  = Path(__file__).resolve()
PROJECT_ROOT = SCRIPT_PATH.parent.parent.parent
DATA_DIR     = PROJECT_ROOT / "data"
LOGS_DIR     = DATA_DIR / "logs"
ARCHIVE_ROOT = LOGS_DIR / "archive"

# Files that will be reset (each has a specific strategy)
TRADE_HISTORY_FILES = [
    LOGS_DIR / "trade_history_eurusd.json",
    LOGS_DIR / "trade_history_usdjpy.json",
]
STATE_FILES = [
    LOGS_DIR / "state_eurusd.json",
    LOGS_DIR / "state_usdjpy.json",
]
# These get deleted outright (they regenerate)
EPHEMERAL_FILES = [
    LOGS_DIR / "account_snapshot.json",
    PROJECT_ROOT / "dashboard_data.json",
]
# Log file gets moved into archive (not deleted, not truncated)
MONITOR_LOG = LOGS_DIR / "live_monitor_v2.log"

# Legacy files — clean them up only if they exist (from pre-dual-pair era)
LEGACY_FILES = [
    LOGS_DIR / "live_state.json",
    LOGS_DIR / "trade_history.json",
    LOGS_DIR / "monthly_pnl.json",
    LOGS_DIR / "live_monitor.log",
]

# Files we MUST NOT touch — if any of these get modified, something is wrong
PROTECTED_FILES = [
    LOGS_DIR / "telegram_recipients.json",
]

# Source files where STARTING_BALANCE constant lives (may need updating)
SOURCE_FILES_WITH_BALANCE = [
    PROJECT_ROOT / "src" / "execution" / "live_signal_monitor_v2.py",
    PROJECT_ROOT / "src" / "execution" / "dashboard_server.py",
]

# Blank state template — matches what the monitor writes on first run
BLANK_STATE = {
    "position_open":   False,
    "position_ticket": None,
    "position_signal": None,
    "entry_price":     None,
    "entry_time":      None,
    "position_lot":    None,
    "last_zscore":     0.0,
    "_armed": {
        "active":       False,
        "direction":    None,
        "target_price": None,
        "armed_time":   None,
        "zscore_abs":   None,
    },
}


# ═══════════════════════════════════════════════════════════════════════════
#  STYLING
# ═══════════════════════════════════════════════════════════════════════════
class C:
    """ANSI colors — degrade to empty strings on non-terminal output."""
    _enabled = sys.stdout.isatty() and os.name != "nt"  # disable on Windows cmd
    # Windows Terminal supports ANSI but classic cmd doesn't; safer to disable.
    # If user's on Windows Terminal they can set FORCE_COLOR=1 to re-enable.
    if os.environ.get("FORCE_COLOR"):
        _enabled = True

    @staticmethod
    def wrap(code: str) -> str:
        return code if C._enabled else ""

    RED    = "\033[31m"
    GREEN  = "\033[32m"
    AMBER  = "\033[33m"
    BLUE   = "\033[34m"
    CYAN   = "\033[36m"
    BOLD   = "\033[1m"
    DIM    = "\033[2m"
    RESET  = "\033[0m"


def paint(text: str, color: str) -> str:
    """Wrap text in an ANSI color if terminal supports it."""
    return f"{C.wrap(color)}{text}{C.wrap(C.RESET)}"


def banner(text: str) -> None:
    """Print a prominent banner."""
    width = 75
    print()
    print(paint("═" * width, C.CYAN))
    print(paint(f"  {text}", C.CYAN + C.BOLD))
    print(paint("═" * width, C.CYAN))


def section(text: str) -> None:
    """Print a section header."""
    print()
    print(paint(f"━━ {text} ", C.BLUE + C.BOLD) + paint("━" * max(0, 72 - len(text)), C.BLUE))


def info(msg: str) -> None:
    print(f"  {msg}")


def ok(msg: str) -> None:
    print(f"  {paint('✓', C.GREEN)} {msg}")


def warn(msg: str) -> None:
    print(f"  {paint('⚠', C.AMBER)} {paint(msg, C.AMBER)}")


def err(msg: str) -> None:
    print(f"  {paint('✗', C.RED)} {paint(msg, C.RED)}")


def dim(msg: str) -> str:
    return paint(msg, C.DIM)


# ═══════════════════════════════════════════════════════════════════════════
#  AUDIT LOGGING
# ═══════════════════════════════════════════════════════════════════════════
@dataclass
class AuditLog:
    """Records everything this script did, saved to manifest.txt in archive."""
    started_at: str = field(default_factory=lambda: datetime.now().isoformat())
    args: list[str] = field(default_factory=list)
    project_root: str = ""
    archive_folder: str = ""
    archived: list[str] = field(default_factory=list)
    reset: list[str] = field(default_factory=list)
    deleted: list[str] = field(default_factory=list)
    preserved: list[str] = field(default_factory=list)
    source_edits: list[str] = field(default_factory=list)
    warnings: list[str] = field(default_factory=list)
    errors: list[str] = field(default_factory=list)
    completed_at: str | None = None

    def to_text(self) -> str:
        lines = [
            "═══════════════════════════════════════════════════════════════════════",
            "  ACCOUNT RESET MANIFEST",
            "═══════════════════════════════════════════════════════════════════════",
            f"  Started   : {self.started_at}",
            f"  Completed : {self.completed_at or 'INCOMPLETE'}",
            f"  Script    : {SCRIPT_PATH}",
            f"  Project   : {self.project_root}",
            f"  Archive   : {self.archive_folder}",
            f"  Args      : {' '.join(self.args) or '(none)'}",
            "",
            "── FILES ARCHIVED ─────────────────────────────────────────────────────",
        ]
        lines.extend(f"  {p}" for p in self.archived) if self.archived else lines.append("  (none)")
        lines.append("")
        lines.append("── FILES RESET (content replaced) ─────────────────────────────────────")
        lines.extend(f"  {p}" for p in self.reset) if self.reset else lines.append("  (none)")
        lines.append("")
        lines.append("── FILES DELETED ──────────────────────────────────────────────────────")
        lines.extend(f"  {p}" for p in self.deleted) if self.deleted else lines.append("  (none)")
        lines.append("")
        lines.append("── FILES PRESERVED (not touched) ──────────────────────────────────────")
        lines.extend(f"  {p}" for p in self.preserved) if self.preserved else lines.append("  (none)")
        lines.append("")
        lines.append("── SOURCE CODE EDITS ──────────────────────────────────────────────────")
        lines.extend(f"  {p}" for p in self.source_edits) if self.source_edits else lines.append("  (none)")
        lines.append("")
        if self.warnings:
            lines.append("── WARNINGS ──────────────────────────────────────────────────────────")
            lines.extend(f"  {p}" for p in self.warnings)
            lines.append("")
        if self.errors:
            lines.append("── ERRORS ────────────────────────────────────────────────────────────")
            lines.extend(f"  {p}" for p in self.errors)
            lines.append("")
        lines.append("═══════════════════════════════════════════════════════════════════════")
        lines.append("  To recover archived files, copy them from the archive folder back")
        lines.append("  to their original locations before restarting the monitor.")
        lines.append("═══════════════════════════════════════════════════════════════════════")
        return "\n".join(lines)


# ═══════════════════════════════════════════════════════════════════════════
#  MONITOR-RUNNING DETECTION
# ═══════════════════════════════════════════════════════════════════════════
def detect_monitor_running() -> list[str]:
    """
    Return a list of process descriptions if the monitor appears to be running.
    Empty list means no monitor found. Never raises — returns empty on any error
    (safer to warn user and let them decide than fail-closed on false positives).
    """
    hits: list[str] = []
    try:
        if os.name == "nt":
            # Windows — use tasklist and parse for python.exe processes,
            # then check their command lines via wmic (if available).
            try:
                out = subprocess.check_output(
                    ["wmic", "process", "where", "name='python.exe'",
                     "get", "ProcessId,CommandLine"],
                    stderr=subprocess.DEVNULL, timeout=5, text=True,
                )
                for line in out.splitlines():
                    if "live_signal_monitor" in line.lower():
                        hits.append(line.strip())
            except Exception:
                # wmic not available on newer Windows — fall back to tasklist
                out = subprocess.check_output(
                    ["tasklist", "/V", "/FO", "CSV"],
                    stderr=subprocess.DEVNULL, timeout=5, text=True,
                )
                for line in out.splitlines():
                    if "python" in line.lower() and "live_signal_monitor" in line.lower():
                        hits.append(line.strip())
        else:
            # Linux/macOS — use ps
            out = subprocess.check_output(
                ["ps", "-eo", "pid,cmd"], stderr=subprocess.DEVNULL,
                timeout=5, text=True,
            )
            for line in out.splitlines():
                if "live_signal_monitor" in line and "grep" not in line:
                    hits.append(line.strip())
    except Exception:
        # If detection itself fails, we can't be sure — return empty and warn.
        pass
    return hits


# ═══════════════════════════════════════════════════════════════════════════
#  FILE OPERATIONS (transactional)
# ═══════════════════════════════════════════════════════════════════════════
def archive_file(src: Path, archive_dir: Path, audit: AuditLog, dry: bool) -> bool:
    """
    Copy a file into the archive folder, preserving its name.
    Returns True if archived, False if source doesn't exist (which is fine).
    """
    if not src.exists():
        info(f"{dim('—')} {src.name} {dim('(not present, skipping)')}")
        return False
    dest = archive_dir / src.name
    if dry:
        info(f"{paint('[DRY]', C.AMBER)} would copy {src} → {dest}")
        audit.archived.append(f"[DRY] {src} → {dest}")
        return True
    try:
        shutil.copy2(src, dest)
        ok(f"archived {src.name} ({dest.stat().st_size:,} bytes)")
        audit.archived.append(f"{src} → {dest}")
        return True
    except Exception as e:
        err(f"archive failed for {src.name}: {e}")
        audit.errors.append(f"Archive {src} failed: {e}")
        return False


def atomic_write_json(path: Path, content, dry: bool) -> bool:
    """Write JSON via a temp file + atomic rename to avoid torn writes."""
    if dry:
        info(f"{paint('[DRY]', C.AMBER)} would write fresh content to {path}")
        return True
    try:
        path.parent.mkdir(parents=True, exist_ok=True)
        tmp = path.with_suffix(path.suffix + ".tmp")
        tmp.write_text(json.dumps(content, indent=2), encoding="utf-8")
        os.replace(str(tmp), str(path))
        return True
    except Exception as e:
        err(f"write failed for {path.name}: {e}")
        return False


def delete_if_exists(path: Path, audit: AuditLog, dry: bool) -> None:
    """Delete a file if present. No-op if absent. Always archived first."""
    if not path.exists():
        info(f"{dim('—')} {path.name} {dim('(already absent)')}")
        return
    if dry:
        info(f"{paint('[DRY]', C.AMBER)} would delete {path}")
        audit.deleted.append(f"[DRY] {path}")
        return
    try:
        path.unlink()
        ok(f"deleted {path.name}")
        audit.deleted.append(str(path))
    except Exception as e:
        err(f"delete failed for {path.name}: {e}")
        audit.errors.append(f"Delete {path} failed: {e}")


# ═══════════════════════════════════════════════════════════════════════════
#  SOURCE CODE: STARTING_BALANCE UPDATE
# ═══════════════════════════════════════════════════════════════════════════
BALANCE_LINE_PATTERN = re.compile(
    r'^(\s*)STARTING_BALANCE\s*=\s*([\d_.]+)(\s*(?:#.*)?)\s*$',
    re.MULTILINE,
)


def find_current_balance() -> float | None:
    """Read the monitor source and return the current STARTING_BALANCE value."""
    src = SOURCE_FILES_WITH_BALANCE[0]  # authoritative = monitor
    if not src.exists():
        return None
    try:
        text = src.read_text(encoding="utf-8")
        m = BALANCE_LINE_PATTERN.search(text)
        if not m:
            return None
        return float(m.group(2).replace("_", ""))
    except Exception:
        return None


def update_balance_in_source(file_path: Path, new_balance: float, audit: AuditLog, dry: bool) -> bool:
    """Atomically rewrite STARTING_BALANCE in a source file. No-op if not found."""
    if not file_path.exists():
        info(f"{dim('—')} {file_path.name} not found, skipping")
        return False
    try:
        text = file_path.read_text(encoding="utf-8")
        matches = BALANCE_LINE_PATTERN.findall(text)
        if not matches:
            info(f"{dim('—')} {file_path.name} has no STARTING_BALANCE line")
            return False
        # Use underscore separator for readability: 100_000.0
        pretty = f"{int(new_balance):,}".replace(",", "_") + ".0"
        new_text = BALANCE_LINE_PATTERN.sub(
            lambda m: f"{m.group(1)}STARTING_BALANCE = {pretty}{m.group(3)}",
            text,
        )
        if new_text == text:
            info(f"{dim('—')} {file_path.name} unchanged (already {pretty})")
            return False
        if dry:
            info(f"{paint('[DRY]', C.AMBER)} would rewrite {len(matches)} occurrence(s) of "
                 f"STARTING_BALANCE → {pretty} in {file_path.name}")
            audit.source_edits.append(
                f"[DRY] {file_path} ({len(matches)} line(s) → {pretty})"
            )
            return True
        # Atomic write
        tmp = file_path.with_suffix(file_path.suffix + ".tmp")
        tmp.write_text(new_text, encoding="utf-8")
        os.replace(str(tmp), str(file_path))
        ok(f"rewrote {len(matches)} line(s) in {file_path.name} → {pretty}")
        audit.source_edits.append(
            f"{file_path} ({len(matches)} line(s) → {pretty})"
        )
        return True
    except Exception as e:
        err(f"source edit failed for {file_path.name}: {e}")
        audit.errors.append(f"Source edit {file_path} failed: {e}")
        return False


# ═══════════════════════════════════════════════════════════════════════════
#  VERIFICATION — read everything back to confirm
# ═══════════════════════════════════════════════════════════════════════════
def verify_reset(new_balance: float, audit: AuditLog) -> list[str]:
    """Re-read all reset files, check they're in the expected state. Returns
    a list of problem descriptions (empty list means everything OK)."""
    problems: list[str] = []

    for p in TRADE_HISTORY_FILES:
        try:
            data = json.loads(p.read_text(encoding="utf-8"))
            if data != []:
                problems.append(f"{p.name} is not empty list: {data!r}")
        except FileNotFoundError:
            problems.append(f"{p.name} missing after reset")
        except Exception as e:
            problems.append(f"{p.name} unreadable: {e}")

    for p in STATE_FILES:
        try:
            data = json.loads(p.read_text(encoding="utf-8"))
            if data.get("position_open") is not False:
                problems.append(f"{p.name} position_open should be False")
            if data.get("position_ticket") is not None:
                problems.append(f"{p.name} position_ticket should be None")
            if not isinstance(data.get("_armed"), dict):
                problems.append(f"{p.name} _armed missing")
            elif data["_armed"].get("active") is not False:
                problems.append(f"{p.name} _armed.active should be False")
        except FileNotFoundError:
            problems.append(f"{p.name} missing after reset")
        except Exception as e:
            problems.append(f"{p.name} unreadable: {e}")

    for p in EPHEMERAL_FILES:
        if p.exists():
            problems.append(f"{p.name} still present (should have been deleted)")

    # Check source files have expected new balance
    current_in_source = find_current_balance()
    if current_in_source is not None and abs(current_in_source - new_balance) > 0.01:
        problems.append(
            f"Monitor source STARTING_BALANCE is {current_in_source}, "
            f"expected {new_balance}"
        )

    # Check protected files are untouched
    for p in PROTECTED_FILES:
        if not p.exists():
            # Not necessarily a problem — might not have been created yet
            audit.warnings.append(f"Protected file {p.name} absent (expected?)")

    return problems


# ═══════════════════════════════════════════════════════════════════════════
#  INTERACTIVE PROMPTS
# ═══════════════════════════════════════════════════════════════════════════
def prompt_typed_confirm(word: str = "RESET") -> bool:
    """Require user to type an exact word to confirm."""
    try:
        print()
        print(paint(f"  Type {word!r} exactly to proceed, or anything else to abort:", C.AMBER))
        reply = input("  > ").strip()
        return reply == word
    except (KeyboardInterrupt, EOFError):
        print()
        return False


def prompt_balance(current: float | None) -> float:
    """Prompt for new starting balance. Default keeps current value."""
    default = current if current else 100_000.0
    print()
    print(f"  Current STARTING_BALANCE in monitor source: "
          f"{paint(f'${default:,.2f}', C.CYAN)}")
    print(f"  Press ENTER to keep current, or type new amount (digits only, no $ or commas):")
    while True:
        try:
            reply = input("  > ").strip()
        except (KeyboardInterrupt, EOFError):
            print()
            raise SystemExit(paint("Aborted.", C.RED))
        if reply == "":
            return default
        try:
            val = float(reply.replace(",", "").replace("_", "").replace("$", ""))
        except ValueError:
            err(f"Not a number: {reply!r}")
            continue
        if val <= 0 or val > 10_000_000:
            err(f"Implausible balance: {val}. Try 100000 for a $100k account.")
            continue
        return val


# ═══════════════════════════════════════════════════════════════════════════
#  MAIN RESET WORKFLOW
# ═══════════════════════════════════════════════════════════════════════════
def main(argv: list[str] | None = None) -> int:
    parser = argparse.ArgumentParser(
        description="Reset the Dual FX Macro Model's account state for a fresh start.",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="See docstring at top of file for detailed behavior.",
    )
    parser.add_argument("--dry-run", action="store_true",
                        help="Preview actions without modifying any files")
    parser.add_argument("--force", action="store_true",
                        help="Skip all interactive confirmation prompts (DANGEROUS)")
    parser.add_argument("--balance", type=float, default=None,
                        help="New STARTING_BALANCE (bypasses interactive prompt). "
                             "Omit to keep current or be prompted.")
    args = parser.parse_args(argv)

    audit = AuditLog()
    audit.args = sys.argv[1:]
    audit.project_root = str(PROJECT_ROOT)

    # ── Banner ─────────────────────────────────────────────────────────────
    banner("DUAL FX MACRO MODEL — ACCOUNT RESET")
    if args.dry_run:
        print(paint("  DRY-RUN MODE — no files will be modified.", C.AMBER + C.BOLD))
    if args.force:
        print(paint("  FORCE MODE — confirmation prompts disabled.", C.RED + C.BOLD))

    # ── Step 1: Locate project and verify structure ────────────────────────
    section("1. Locating project structure")
    info(f"Project root : {PROJECT_ROOT}")
    info(f"Logs folder  : {LOGS_DIR}")

    if not PROJECT_ROOT.exists():
        err(f"Project root doesn't exist: {PROJECT_ROOT}")
        return 2
    if not LOGS_DIR.exists():
        warn(f"Logs folder doesn't exist yet: {LOGS_DIR}")
        warn("This looks like a fresh install — nothing to reset.")
        if not args.dry_run:
            try:
                LOGS_DIR.mkdir(parents=True, exist_ok=True)
                ok(f"created {LOGS_DIR}")
            except Exception as e:
                err(f"failed to create logs folder: {e}")
                return 2
    else:
        ok(f"logs folder found")

    current_balance = find_current_balance()
    if current_balance is None:
        warn("Could not determine current STARTING_BALANCE from monitor source.")
        warn("You'll be prompted for a new value; no source edit will happen if "
             "source file isn't found.")
    else:
        ok(f"current STARTING_BALANCE in source: ${current_balance:,.2f}")

    # ── Step 2: Check monitor isn't running ────────────────────────────────
    section("2. Checking monitor process")
    hits = detect_monitor_running()
    if hits:
        err("Monitor process appears to be RUNNING:")
        for h in hits[:5]:
            print(f"    {dim(h[:120])}")
        print()
        err("You MUST stop the monitor (Ctrl+C in its terminal) before resetting.")
        err("Otherwise the monitor will immediately recreate state files with")
        err("live MT5 data, defeating the reset.")
        if not args.force:
            return 3
        warn("--force supplied; proceeding despite running monitor (NOT recommended)")
        audit.warnings.append(f"Monitor detected running: {hits}")
    else:
        ok("no live_signal_monitor process detected")

    # ── Step 3: Show what will happen ──────────────────────────────────────
    section("3. Reset plan")
    print("  The following will be ARCHIVED then reset or deleted:")
    print()
    for p in TRADE_HISTORY_FILES + STATE_FILES + EPHEMERAL_FILES + [MONITOR_LOG]:
        marker = paint("✎", C.AMBER) if p.exists() else paint("·", C.DIM)
        status = dim("(missing, skip)") if not p.exists() else ""
        print(f"    {marker} {p.relative_to(PROJECT_ROOT)} {status}")
    legacy_present = [p for p in LEGACY_FILES if p.exists()]
    if legacy_present:
        print()
        print(f"  Legacy files will also be archived and deleted:")
        for p in legacy_present:
            print(f"    {paint('✎', C.AMBER)} {p.relative_to(PROJECT_ROOT)} {dim('(legacy)')}")
    print()
    print("  The following will be PRESERVED:")
    for p in PROTECTED_FILES:
        present = dim("(present)") if p.exists() else dim("(absent)")
        print(f"    {paint('✓', C.GREEN)} {p.relative_to(PROJECT_ROOT)} {present}")

    # ── Step 4: Confirm ────────────────────────────────────────────────────
    if not args.force:
        section("4. Confirmation")
        if not prompt_typed_confirm("RESET"):
            print()
            print(paint("  Aborted — no files were touched.", C.AMBER))
            return 1
    else:
        section("4. Confirmation")
        info("--force supplied, skipping prompt")

    # ── Step 5: Prompt for starting balance ────────────────────────────────
    section("5. Starting balance for new account")
    if args.balance is not None:
        new_balance = args.balance
        info(f"--balance ${new_balance:,.2f} supplied")
    elif args.force:
        new_balance = current_balance if current_balance else 100_000.0
        info(f"--force and no --balance given; keeping current "
             f"${new_balance:,.2f}")
    else:
        new_balance = prompt_balance(current_balance)
        ok(f"new STARTING_BALANCE will be ${new_balance:,.2f}")

    # ── Step 6: Create archive folder ──────────────────────────────────────
    section("6. Creating archive folder")
    timestamp = datetime.now().strftime("%Y-%m-%d_%H%M%S")
    archive_dir = ARCHIVE_ROOT / f"reset_{timestamp}"
    audit.archive_folder = str(archive_dir)
    if args.dry_run:
        info(f"{paint('[DRY]', C.AMBER)} would create {archive_dir}")
    else:
        try:
            archive_dir.mkdir(parents=True, exist_ok=True)
            ok(f"created {archive_dir}")
        except Exception as e:
            err(f"failed to create archive folder: {e}")
            return 4

    # ── Step 7: Archive everything that will be touched ────────────────────
    section("7. Archiving current state")
    archive_targets = (TRADE_HISTORY_FILES + STATE_FILES + EPHEMERAL_FILES
                       + [MONITOR_LOG] + legacy_present)
    for p in archive_targets:
        archive_file(p, archive_dir, audit, args.dry_run)

    # ── Step 8: Reset trade histories ──────────────────────────────────────
    section("8. Resetting trade histories → []")
    for p in TRADE_HISTORY_FILES:
        if atomic_write_json(p, [], args.dry_run):
            if not args.dry_run:
                ok(f"wrote {p.name} = []")
            audit.reset.append(str(p))

    # ── Step 9: Reset state files → blank template ─────────────────────────
    section("9. Resetting state files → blank")
    for p in STATE_FILES:
        if atomic_write_json(p, BLANK_STATE, args.dry_run):
            if not args.dry_run:
                ok(f"wrote {p.name} = blank template")
            audit.reset.append(str(p))

    # ── Step 10: Delete ephemeral files ────────────────────────────────────
    section("10. Deleting ephemeral files")
    for p in EPHEMERAL_FILES:
        delete_if_exists(p, audit, args.dry_run)

    # ── Step 11: Rotate monitor log ────────────────────────────────────────
    section("11. Rotating monitor log")
    if MONITOR_LOG.exists():
        if args.dry_run:
            info(f"{paint('[DRY]', C.AMBER)} would truncate {MONITOR_LOG.name} "
                 f"(already archived)")
        else:
            try:
                # Already archived in step 7. Truncate the live file.
                MONITOR_LOG.write_text("", encoding="utf-8")
                ok(f"truncated {MONITOR_LOG.name} (full history in archive)")
                audit.reset.append(str(MONITOR_LOG))
            except Exception as e:
                err(f"failed to truncate log: {e}")
                audit.errors.append(f"Truncate {MONITOR_LOG} failed: {e}")
    else:
        info(f"{dim('—')} {MONITOR_LOG.name} not present")

    # ── Step 12: Delete legacy files outright ──────────────────────────────
    if legacy_present:
        section("12. Removing legacy files (already archived)")
        for p in legacy_present:
            delete_if_exists(p, audit, args.dry_run)

    # ── Step 13: Update STARTING_BALANCE if changed ────────────────────────
    section("13. Updating STARTING_BALANCE in source code")
    if current_balance is not None and abs(current_balance - new_balance) < 0.01:
        info("balance unchanged; no source edits needed")
    else:
        for src in SOURCE_FILES_WITH_BALANCE:
            # Archive source file before editing
            if src.exists() and not args.dry_run:
                try:
                    shutil.copy2(src, archive_dir / src.name)
                    audit.archived.append(f"{src} → {archive_dir / src.name}")
                except Exception as e:
                    err(f"failed to archive {src.name} before edit: {e}")
                    audit.errors.append(f"Source archive {src} failed: {e}")
                    continue
            update_balance_in_source(src, new_balance, audit, args.dry_run)

    # ── Step 14: Note preserved files ──────────────────────────────────────
    section("14. Preserved files")
    for p in PROTECTED_FILES:
        if p.exists():
            ok(f"{p.relative_to(PROJECT_ROOT)} untouched ({p.stat().st_size} bytes)")
            audit.preserved.append(str(p))
        else:
            info(f"{dim('—')} {p.relative_to(PROJECT_ROOT)} absent (nothing to preserve)")

    # ── Step 15: Verification ──────────────────────────────────────────────
    section("15. Verifying reset state")
    if args.dry_run:
        info(f"{paint('[DRY]', C.AMBER)} skipping verification in dry-run mode")
    else:
        problems = verify_reset(new_balance, audit)
        if problems:
            err(f"verification found {len(problems)} problem(s):")
            for msg in problems:
                print(f"    {paint('✗', C.RED)} {msg}")
                audit.errors.append(f"Verify: {msg}")
        else:
            ok("all reset files pass verification")
            # Extra sanity: show the fresh state
            try:
                eu_hist = json.loads((LOGS_DIR / "trade_history_eurusd.json").read_text())
                uj_hist = json.loads((LOGS_DIR / "trade_history_usdjpy.json").read_text())
                info(f"EURUSD trades: {len(eu_hist)} · USDJPY trades: {len(uj_hist)}")
                new_src_balance = find_current_balance()
                if new_src_balance:
                    info(f"Monitor STARTING_BALANCE: ${new_src_balance:,.2f}")
            except Exception:
                pass

    # ── Step 16: Write audit manifest into archive folder ──────────────────
    section("16. Writing audit manifest")
    audit.completed_at = datetime.now().isoformat()
    if args.dry_run:
        info(f"{paint('[DRY]', C.AMBER)} would write manifest to {archive_dir / 'manifest.txt'}")
    else:
        try:
            (archive_dir / "manifest.txt").write_text(audit.to_text(), encoding="utf-8")
            ok(f"wrote {archive_dir / 'manifest.txt'}")
        except Exception as e:
            err(f"failed to write manifest: {e}")

    # ── Final summary ──────────────────────────────────────────────────────
    banner("RESET COMPLETE" if not args.dry_run else "DRY-RUN COMPLETE (no changes made)")
    if audit.errors:
        print(paint(f"  ⚠ {len(audit.errors)} error(s) during reset — review manifest.txt",
                    C.RED + C.BOLD))
    print()
    print(f"  Archive folder : {paint(str(archive_dir), C.CYAN)}")
    print(f"  Files archived : {len(audit.archived)}")
    print(f"  Files reset    : {len(audit.reset)}")
    print(f"  Files deleted  : {len(audit.deleted)}")
    print(f"  Source edits   : {len(audit.source_edits)}")
    print()
    print(paint("  Next steps:", C.BOLD))
    print(f"    1. Update MT5 credentials in the monitor (new FTMO account login)")
    print(f"    2. Verify MT5 connects to the correct broker/server")
    print(f"    3. Start the monitor:")
    print(f"       {dim('python live_signal_monitor_v2.py')}")
    print(f"    4. Start the dashboard:")
    print(f"       {dim('python dashboard_server.py')}")
    print(f"    5. Watch the first signal check — should show:")
    print(f"       {dim('balance=$' + f'{new_balance:,.2f}')} · "
          f"{dim('P&L $+0.00 · trades=0')}")
    print()
    print(paint("  Remember: your trade journal (trade_log_v2.xlsx) is separate and", C.AMBER))
    print(paint("  not affected by this reset. Clear it manually if needed.", C.AMBER))
    print()

    return 0 if not audit.errors else 5


if __name__ == "__main__":
    try:
        sys.exit(main())
    except KeyboardInterrupt:
        print()
        print(paint("  Interrupted. Partial state may exist — check archive folder.", C.AMBER))
        sys.exit(130)
