#!/usr/bin/env python3
"""
SC-F001 Log Tool

Usage:
  logtool.py <source> [options]

  <source>   *.bin file path  →  read local file
             anything else    →  treated as hostname/URL:
               sc.local           →  http://sc.local/log
               192.168.4.1        →  http://192.168.4.1/log
               http://host/log    →  used as-is

Output files are always written:
  <basename>.bin   raw bytes received (HTTP response or file contents)
  <basename>.txt   stdout capture (table / summary)

Default basename: 04MAR2026_1052 (date+time of invocation).
Specify with --out <basename>.

Options:
  --gui                Show matplotlib plots instead of CLI table
  --stream             Poll for new entries (HTTP only)
  --type fsm|bat|crash Filter entry type
  --tail <offset>      Start from a specific flash offset (HTTP POST mode)
  --interval <s>       Polling interval in seconds (default: 2.0)
  --fw <path>          Path to firmware main/ directory (for state names)
  --summary            Print summary statistics only
  --out <basename>     Output file basename (no extension)
"""

import sys
import io
import argparse
from datetime import datetime
from pathlib import Path

# Ensure logtool directory is on the path
sys.path.insert(0, str(Path(__file__).parent))

import parser as prs
import source as src
import cli_view
import gui_view


def _default_basename() -> str:
    return datetime.now().strftime('%d%b%Y_%H%M').upper()


def _normalize_source(raw: str) -> tuple:
    """
    Returns (is_http: bool, resolved: str).
    *.bin → (False, path)
    http(s)://...  → (True, url as-is)
    anything else  → (True, http://<raw>/log)
    """
    if raw.endswith('.bin'):
        return False, raw
    if raw.startswith('http://') or raw.startswith('https://'):
        return True, raw
    return True, f'http://{raw}/log'


class _Tee:
    """Write to two streams simultaneously (stdout + file)."""
    def __init__(self, primary, secondary):
        self.primary   = primary
        self.secondary = secondary

    def write(self, data):
        self.primary.write(data)
        self.secondary.write(data)
        return len(data)

    def flush(self):
        self.primary.flush()
        self.secondary.flush()

    def __getattr__(self, name):
        return getattr(self.primary, name)


def main():
    ap = argparse.ArgumentParser(
        description='SC-F001 flash log viewer',
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog=__doc__
    )
    ap.add_argument('source', help='*.bin file, hostname, or full URL')
    ap.add_argument('--gui', action='store_true', help='Show matplotlib GUI')
    ap.add_argument('--stream', action='store_true', help='Live streaming mode (HTTP only)')
    ap.add_argument('--type', choices=['fsm', 'bat', 'crash'], dest='entry_type',
                    help='Filter by entry type')
    ap.add_argument('--tail', type=int, default=None,
                    help='Start from flash offset (HTTP only)')
    ap.add_argument('--interval', type=float, default=2.0,
                    help='Polling interval in seconds (default: 2.0)')
    ap.add_argument('--fw', default=None,
                    help='Path to firmware main/ directory (default: ../main)')
    ap.add_argument('--summary', action='store_true',
                    help='Print summary statistics only')
    ap.add_argument('--out', default=None, metavar='BASENAME',
                    help='Output file basename (default: 04MAR2026_1052 style)')
    args = ap.parse_args()

    # Resolve source
    is_http, resolved = _normalize_source(args.source)

    # Output file basename
    basename = args.out or _default_basename()
    bin_path = Path(basename + '.bin')
    txt_path = Path(basename + '.txt')

    # Tee stdout → .txt file
    txt_file = txt_path.open('w', encoding='utf-8')
    sys.stdout = _Tee(sys.__stdout__, txt_file)

    try:
        _run(args, is_http, resolved, bin_path, basename)
    finally:
        sys.stdout = sys.__stdout__
        txt_file.close()
        print(f"Saved: {bin_path}  {txt_path}")


def _run(args, is_http, resolved, bin_path, basename):
    # Load FSM state names from firmware source
    fw_path = args.fw or (Path(__file__).parent.parent / 'main')
    fsm_states = prs.load_fsm_states(fw_path)

    # ── Streaming mode ────────────────────────────────────────────────────────
    if args.stream:
        if not is_http:
            print("Error: --stream requires an HTTP source", file=sys.stderr)
            sys.exit(1)

        if args.gui:
            print(f"Starting live GUI from {resolved} ...")
            # GUI handles its own data fetching; raw bytes aren't easily capturable
            bin_path.write_bytes(b'')
            gui_view.live_plot(resolved, interval_s=args.interval)
            return

        print(f"Streaming from {resolved} (Ctrl-C to stop)\n")
        print('  '.join(cli_view._HEADERS))
        print('  '.join('-' * len(h) for h in cli_view._HEADERS))

        accumulated_bin = io.BytesIO()

        def on_batch(entries, meta, is_first):
            if args.entry_type:
                tf = args.entry_type.lower()
                if tf == 'fsm':
                    entries = [e for e in entries if 0 <= e.get('entry_type', -1) <= 12]
                elif tf == 'bat':
                    entries = [e for e in entries if e.get('entry_type') == prs.LOG_TYPE_BAT]
                elif tf == 'crash':
                    entries = [e for e in entries if e.get('entry_type') == prs.LOG_TYPE_CRASH]
            if entries:
                cli_view.append_rows(entries)

        def _patched_stream():
            """Like source.stream but also captures raw bytes."""
            blob = src.fetch_full(resolved)
            accumulated_bin.write(blob)
            meta, tail, head, entries = prs.autodetect_and_parse(blob, fsm_states)
            on_batch(entries, meta, is_first=True)

            current_tail = head or 0
            import time
            while True:
                time.sleep(args.interval)
                try:
                    binary, new_head = src.fetch_incremental(resolved, current_tail)
                    if binary:
                        accumulated_bin.write(binary)
                        new_entries = prs.parse_entries(binary, fsm_states)
                        if new_entries:
                            on_batch(new_entries, None, is_first=False)
                    current_tail = new_head
                except Exception as exc:
                    print(f"[stream] poll error: {exc}")

        try:
            _patched_stream()
        except KeyboardInterrupt:
            print("\nStopped.")
        finally:
            bin_path.write_bytes(accumulated_bin.getvalue())
        return

    # ── One-shot mode ─────────────────────────────────────────────────────────
    if is_http:
        print(f"Fetching {resolved} ...")
        if args.tail is not None:
            binary, new_head = src.fetch_incremental(resolved, args.tail)
            blob = binary
            entries = prs.parse_entries(binary, fsm_states)
            meta = None
            print(f"Incremental fetch: new_head={new_head}  entries={len(entries)}")
        else:
            blob = src.fetch_full(resolved)
            meta, tail, head, entries = prs.parse_response(blob, fsm_states)
            print(f"Log offsets: tail={tail}  head={head}  entries={len(entries)}")
    else:
        print(f"Reading {resolved} ...")
        blob = src.read_file(resolved)
        meta, tail, head, entries = prs.autodetect_and_parse(blob, fsm_states)
        if head is not None:
            print(f"Log offsets: tail={tail}  head={head}")
        print(f"Parsed {len(entries)} entries")

    # Save raw binary
    bin_path.write_bytes(blob or b'')

    if meta:
        ver = meta.get('version', '?')
        t   = meta.get('time', '?')
        print(f"Device: version={ver}  time={t}")

    if args.summary:
        cli_view.print_summary(entries)
    elif args.gui:
        title = f"SC-F001 Log — {args.source}"
        gui_view.show_plots(entries, title=title)
    else:
        cli_view.print_table(entries, type_filter=args.entry_type)
        print()
        cli_view.print_summary(entries)


if __name__ == '__main__':
    main()
