#!/usr/bin/env python3
"""
Post-scan health + compliance check. Runs right after run_daily_scan.sh's
Claude invocation finishes and answers two questions that nothing previously
verified:

  1. Did the scan actually produce valid output today? (daily_run.log once
     showed a run that "started but never finished" with no alert to anyone --
     found only by manual log review.)
  2. Did it actually follow daily_scan_prompt.md's Step 1a/2 requirements
     (apply active lessons, evaluate enough candidates/sectors), or just
     silently under-comply the way Step 1b's enrichment did for two months?

Both failure modes are the same root problem: trusting a prompt was followed
without checking. This script checks.

Run manually:  python3 verify_scan.py
Cron (right after run_daily_scan.sh finishes, appended to that script itself
rather than a separate cron entry, so it can't run before the scan is done).
"""
import json
import sys
from datetime import date
from pathlib import Path

BASE = Path(__file__).parent
sys.path.insert(0, str(BASE))
from is_trading_day import is_trading_day
from ntfy_alert import send_alert

if not is_trading_day():
    print(f'[verify_scan] {date.today()} is not a TSX trading day -- skipping')
    sys.exit(0)

SNAPSHOTS_DIR      = BASE / 'daily_snapshots'
CRITERIA_FILE      = BASE / 'screen_criteria.json'
RETROSPECTIVE_FILE = BASE / 'retrospective.json'

REQUIRED_FIELDS = [
    'scan_date', 'macro_notes', 'candidates_evaluated_count',
    'sectors_evaluated', 'lessons_applied', 'signals', 'excluded',
]
REQUIRED_SIGNAL_KEYS = ['new_buys', 'holds', 'exits', 'watchlist']

# Kept in sync with daily_scan_prompt.md's Step 6 order_status enum -- add a
# new value in both places, never just here or just there.
KNOWN_ORDER_STATUSES = {
    'filled', 'rejected', 'denied', 'timed_out_unfilled',
    'skipped_pyramid', 'skipped_hard_guardrail', 'skipped_soft_guardrail',
    'skipped_preview_rejected', 'aborted_account_ambiguous',
    'aborted_reconciliation_mismatch', 'aborted_mcp_tools_unavailable',
}


def load_json(path, default=None):
    if not path.exists():
        return default
    with open(path) as f:
        return json.load(f)


def main():
    today = date.today().isoformat()
    failures = []

    snap_path = SNAPSHOTS_DIR / f'swing_trade_{today}.json'
    if not snap_path.exists():
        send_alert(
            f'Daily scan produced no output file for {today}. It may have crashed '
            f'or timed out mid-run -- check daily_run.log.',
            title='SwingTrader: Scan Did Not Run',
        )
        print(f'[verify_scan] FAIL -- no output file for {today}')
        sys.exit(1)

    try:
        with open(snap_path) as f:
            snap = json.load(f)
    except json.JSONDecodeError as e:
        send_alert(
            f"Today's scan output ({snap_path.name}) is not valid JSON: {e}. "
            'The scan likely got cut off mid-write -- check daily_run.log.',
            title='SwingTrader: Scan Output Malformed',
        )
        print(f'[verify_scan] FAIL -- invalid JSON in {snap_path.name}: {e}')
        sys.exit(1)

    for field in REQUIRED_FIELDS:
        if field not in snap:
            failures.append(f'missing required field "{field}"')

    signals = snap.get('signals') or {}
    for key in REQUIRED_SIGNAL_KEYS:
        if key not in signals:
            failures.append(f'signals missing "{key}"')

    # Step 6 (auto-execution) writes order_status on every new_buys entry.
    # This check is deliberately independent of whatever the scan's own
    # macro_notes/ntfy claims -- 2026-08-27 showed the scan can produce an
    # off-schema order_status (a real new_buy silently not executed because
    # the Questrade MCP connector wasn't available to the cron job) while
    # its own text claimed an alert had already been sent that never arrived.
    # This fires its own urgent alert regardless of that claim.
    new_buys = signals.get('new_buys') or []
    bad_status_tickers = [
        e.get('ticker', '?') for e in new_buys
        if e.get('order_status') not in KNOWN_ORDER_STATUSES
    ]
    if bad_status_tickers:
        send_alert(
            f"Today's scan ({today}) produced new_buys for "
            f"{', '.join(bad_status_tickers)} but Step 6 order execution did not "
            f"reach a recognized order_status. This most likely means the "
            f"Questrade MCP connector was not available to the cron job and "
            f"these buy candidates were NOT placed -- check "
            f"daily_snapshots/swing_trade_{today}.json and place manually via "
            f"place_trade_prompt.md if still valid.",
            title='SwingTrader: New-Buy Execution Did Not Complete',
            priority='urgent',
        )
        failures.append(
            f'new_buys order_status not recognized for: {", ".join(bad_status_tickers)}'
        )

    if snap.get('scan_date') != today:
        failures.append(f'scan_date is "{snap.get("scan_date")}", expected "{today}"')

    criteria = load_json(CRITERIA_FILE, {}) or {}
    funnel = criteria.get('funnel_breadth', {})
    min_candidates = funnel.get('min_candidates_evaluated_per_scan')
    min_sectors    = funnel.get('min_sectors_represented')

    count = snap.get('candidates_evaluated_count')
    if min_candidates is not None:
        if count is None:
            failures.append('candidates_evaluated_count was not filled in')
        elif count < min_candidates:
            failures.append(
                f'only evaluated {count} candidates, below the {min_candidates} minimum'
            )

    sectors = snap.get('sectors_evaluated')
    if min_sectors is not None:
        if not isinstance(sectors, list):
            failures.append('sectors_evaluated was not filled in')
        elif len(sectors) < min_sectors:
            failures.append(
                f'only {len(sectors)} sector(s) evaluated ({sectors}), below the {min_sectors} minimum'
            )

    # retrospective.json at this point still reflects yesterday's 2:05pm run --
    # today's doesn't regenerate until after this check -- so this is exactly
    # the lessons the scan was supposed to have read and applied.
    retro = load_json(RETROSPECTIVE_FILE, None)
    if retro and retro.get('lessons_for_next_scan'):
        if not snap.get('lessons_applied'):
            failures.append(
                f'{len(retro["lessons_for_next_scan"])} active lesson(s) existed but '
                'lessons_applied is empty in today\'s output'
            )

    if failures:
        summary = '; '.join(failures)
        send_alert(
            f"Today's scan ({today}) ran but didn't fully comply: {summary}",
            title='SwingTrader: Scan Compliance Issue',
        )
        print(f'[verify_scan] {len(failures)} compliance issue(s): {summary}')
        sys.exit(1)

    print(f'[verify_scan] OK -- {today} scan valid and compliant '
          f'({count} candidates, {len(sectors or [])} sectors)')


if __name__ == '__main__':
    main()
