"""Output formatting for CLI results (Layer 2).

Formats structured SDK events for agent consumption.

Progress events print immediately as they arrive (via on_event callbacks).
Completion events print the final result after the stream closes.
"""

from __future__ import annotations

from typing import cast

from .client import StreamResult
from .parse import JSONObject, SDKEvent, parse_json_field

# ---- Helpers ----


def _print_footer(
    code: int, elapsed: float, result: StreamResult, command: str | None = None
) -> None:
    """Print the [exit:N | Xs] footer with optional run_id and resume hint."""
    footer = f'[exit:{code} | {elapsed:.1f}s]'
    if result.run_id:
        footer += f'  {result.run_id}'
    print(footer)
    if result.run_id and command:
        print(f'\nresume: ansel {command} {result.run_id}')


def _str(val: object, default: str = '?') -> str:
    """Coerce a JSONObject value to str, with a fallback default."""
    if val is None:
        return default
    return str(val)


def _list(val: object) -> list[JSONObject]:
    """Coerce a JSONObject value to a list of dicts."""
    if isinstance(val, list):
        return [
            cast(JSONObject, item) for item in cast(list[object], val) if isinstance(item, dict)
        ]
    return []


# ---- Shared formatters ----


def format_file_line(f: JSONObject) -> str:
    """Format a single file as a one-line summary."""
    file_id: str = _str(f.get('id'))
    name: str = _str(f.get('name'))
    records = ', '.join(parse_json_field(f.get('records')))
    aggs = ', '.join(parse_json_field(f.get('aggregations')))
    freqs = ', '.join(parse_json_field(f.get('frequencies')))

    # Handle both JSONL event format and D1 format for date range
    dr = f.get('date_range')
    dr_min = f.get('date_range_min')
    dr_max = f.get('date_range_max')
    if dr and isinstance(dr, dict):
        dr_dict = cast(JSONObject, dr)
        date_str = f'{_str(dr_dict.get("min"))[:7]} -> {_str(dr_dict.get("max"))[:7]}'
    elif dr_min and dr_max:
        date_str = f'{str(dr_min)[:7]} -> {str(dr_max)[:7]}'
    else:
        date_str = '-'

    created_at = str(f.get('created_at', ''))[:16]  # "2026-03-17 23:47"
    parts = [f'  {file_id}', name, records, aggs, freqs, date_str]
    if created_at:
        parts.append(created_at)
    return '  '.join(parts)


# ---- Streaming callbacks (print as events arrive) ----


def on_files_event(event: SDKEvent, result: StreamResult) -> None:
    """Print files progress events as they arrive."""
    name = event['event']
    data = event['data']

    if name == 'FilesDiscovered':
        count = len(_list(data.get('files')))
        run_id = result.run_id or ''
        prefix = f'{run_id}: ' if run_id else ''
        print(f'{prefix}classifying {count} file{"s" if count != 1 else ""}', flush=True)

    elif name == 'FilesClassificationCompleted':
        files = _list(data.get('files'))
        for f in files:
            print(format_file_line(f), flush=True)

    elif name == 'FilesClassificationFailed':
        error: str = _str(data.get('error'), 'unknown error')
        print(f'error: classification failed — {error}', flush=True)


def on_forecast_event(event: SDKEvent, result: StreamResult) -> None:
    """Print forecast progress events as they arrive."""
    name = event['event']
    data = event['data']

    if name == 'FilesDiscovered':
        count = len(_list(data.get('files')))
        run_id = result.run_id or ''
        prefix = f'{run_id}: ' if run_id else ''
        print(f'{prefix}classifying {count} file{"s" if count != 1 else ""}', flush=True)

    elif name == 'FilesClassificationCompleted':
        files = _list(data.get('files'))
        print(f'classified {len(files)} file{"s" if len(files) != 1 else ""}', flush=True)

    elif name == 'SeriesExtractionStarted':
        print('extracting series', flush=True)

    elif name == 'ModelingStarted':
        series = _list(data.get('series'))
        print(f'training {len(series)} model{"s" if len(series) != 1 else ""}', flush=True)

    elif name == 'ForecastingStarted':
        series = _list(data.get('series'))
        print(f'forecasting {len(series)} series', flush=True)

    elif name == 'ForecastGenCompleted':
        series_name = _str(data.get('series_name', data.get('series_id', '?')))
        print(f'  \u2713  {series_name}', flush=True)

    elif name == 'ForecastGenFailed' or name == 'TargetDataFailed':
        series_name = _str(data.get('series_name', data.get('series_id', '?')))
        error_detail = _str(data.get('error'), 'unknown error')
        print(f'  \u2717  {series_name}  {error_detail}', flush=True)

    elif name == 'FilesClassificationFailed':
        error_msg: str = _str(data.get('error'), 'unknown error')
        print(f'error: classification failed — {error_msg}', flush=True)


# ---- Final result printers (after stream closes) ----


def print_files_result(result: StreamResult, elapsed: float) -> int:
    """Print final footer for files command. Returns exit code.

    Progress events are already printed by on_files_event during streaming.
    This only handles errors and the footer.
    """
    code = result.exit_code if result.exit_code is not None else 1

    if result.errors:
        for err in result.errors:
            print(f'error: {err}')
        _print_footer(code, elapsed, result)
        return code

    if code != 0 and not result.get_event('FilesClassificationFailed') and result.stderr.strip():
        print(f'error: {result.stderr.strip().splitlines()[-1]}')

    _print_footer(code, elapsed, result)
    return code


def format_series_line(s: JSONObject) -> str:
    """Format a single series as a one-line summary."""
    series_id: str = _str(s.get('id'))
    name: str = _str(s.get('name'))
    aggregation: str = _str(s.get('aggregation'), '')

    # Identifiers: from SDK event (list of dicts) or D1 (JSON string)
    raw_identifiers: object = s.get('identifiers', [])
    if isinstance(raw_identifiers, str):
        import json

        try:
            raw_identifiers = json.loads(raw_identifiers)  # pyright: ignore[reportAny]
        except (json.JSONDecodeError, TypeError):
            raw_identifiers = []

    if isinstance(raw_identifiers, list) and raw_identifiers:
        id_parts: list[str] = []
        for ident in cast(list[object], raw_identifiers):
            if isinstance(ident, dict):
                ident_dict = cast(JSONObject, ident)
                id_parts.append(
                    f'{_str(ident_dict.get("type"), "")}: {_str(ident_dict.get("value"), "")}'
                )
            else:
                id_parts.append(str(ident))
        id_str = ', '.join(id_parts)
    else:
        id_str = ''

    parts = [f'  {series_id}', name, aggregation]
    if id_str:
        parts.append(id_str)
    created_at = str(s.get('created_at', ''))[:16]
    if created_at:
        parts.append(created_at)
    return '  '.join(parts)


def on_series_event(event: SDKEvent, result: StreamResult) -> None:
    """Print series progress events as they arrive."""
    name = event['event']
    data = event['data']

    if name == 'FilesDiscovered':
        count = len(_list(data.get('files')))
        run_id = result.run_id or ''
        prefix = f'{run_id}: ' if run_id else ''
        print(f'{prefix}classifying {count} file{"s" if count != 1 else ""}', flush=True)

    elif name == 'FilesClassificationCompleted':
        files = _list(data.get('files'))
        print(f'classified {len(files)} file{"s" if len(files) != 1 else ""}', flush=True)

    elif name == 'SeriesExtractionStarted':
        limit = data.get('limit', '')
        suffix = f' (limit {limit})' if limit else ''
        print(f'extracting series{suffix}', flush=True)

    elif name == 'SeriesExtractionCompleted':
        series = _list(data.get('series'))
        for s in series:
            print(format_series_line(s), flush=True)

    elif name == 'SeriesExtractionFailed':
        error: str = _str(data.get('error'), 'unknown error')
        print(f'error: series extraction failed — {error}', flush=True)

    elif name == 'FilesClassificationFailed':
        error_msg: str = _str(data.get('error'), 'unknown error')
        print(f'error: classification failed — {error_msg}', flush=True)


def print_series_result(result: StreamResult, elapsed: float) -> int:
    """Print final footer for series command. Returns exit code."""
    code = result.exit_code if result.exit_code is not None else 1

    if result.errors:
        for err in result.errors:
            print(f'error: {err}')
        _print_footer(code, elapsed, result, 'series')
        return code

    if code != 0 and not result.get_event('SeriesExtractionFailed') and result.stderr.strip():
        print(f'error: {result.stderr.strip().splitlines()[-1]}')

    _print_footer(code, elapsed, result, 'series')
    return code


def format_model_line(m: JSONObject) -> str:
    """Format a single model as a one-line summary."""
    model_id: str = _str(m.get('id'))
    series_id: str = _str(m.get('series_id'))
    series_name: str = _str(m.get('series_name'), '')
    series_str = f'{series_name} ({series_id})' if series_name else series_id
    target: str = _str(m.get('target'), '')
    interval: str = _str(m.get('interval'), '')
    horizon = _str(m.get('horizon'), '')
    created_at = str(m.get('created_at', ''))[:16]
    parts = [f'  {model_id}', series_str]
    if target:
        parts.append(target)
    if horizon and interval:
        parts.append(f'{horizon} {interval}s')
    if created_at:
        parts.append(created_at)
    return '  '.join(parts)


def on_models_event(event: SDKEvent, result: StreamResult) -> None:
    """Print model training progress events as they arrive."""
    name = event['event']
    data = event['data']

    if name == 'FilesDiscovered':
        count = len(_list(data.get('files')))
        run_id = result.run_id or ''
        prefix = f'{run_id}: ' if run_id else ''
        print(f'{prefix}classifying {count} file{"s" if count != 1 else ""}', flush=True)

    elif name == 'FilesClassificationCompleted':
        files = _list(data.get('files'))
        print(f'classified {len(files)} file{"s" if len(files) != 1 else ""}', flush=True)

    elif name == 'SeriesExtractionStarted':
        print('extracting series', flush=True)

    elif name == 'SeriesExtractionCompleted':
        series = _list(data.get('series'))
        print(f'extracted {len(series)} series', flush=True)

    elif name == 'ModelingStarted':
        series = _list(data.get('series'))
        print(f'training {len(series)} model{"s" if len(series) != 1 else ""}', flush=True)

    elif name == 'DataPrepCompleted':
        series_name = _str(data.get('series_name', data.get('series_id', '?')))
        print(f'  \u2713 data prep: {series_name}', flush=True)

    elif name == 'DataPrepFailed':
        series_name = _str(data.get('series_name', data.get('series_id', '?')))
        error_detail = _str(data.get('error'), 'unknown error')
        print(f'  \u2717 data prep: {series_name}  {error_detail}', flush=True)

    elif name == 'ModelTrainCompleted':
        series_name = _str(data.get('series_name', data.get('series_id', '?')))
        print(f'  \u2713 trained: {series_name}', flush=True)

    elif name == 'ModelTrainFailed':
        series_name = _str(data.get('series_name', data.get('series_id', '?')))
        error_detail = _str(data.get('error'), 'unknown error')
        print(f'  \u2717 training: {series_name}  {error_detail}', flush=True)

    elif name == 'FilesClassificationFailed':
        error_msg: str = _str(data.get('error'), 'unknown error')
        print(f'error: classification failed — {error_msg}', flush=True)


def print_models_result(result: StreamResult, elapsed: float) -> int:
    """Print final result for models command. Returns exit code.

    Progress and per-series events are already printed by on_models_event.
    This prints the summary table from ModelingCompleted and the footer.
    """
    code = result.exit_code if result.exit_code is not None else 1

    if result.errors:
        for err in result.errors:
            print(f'error: {err}')
        _print_footer(code, elapsed, result, 'models')
        return code

    completed = result.get_event('ModelingCompleted')
    if completed is not None:
        models_data = _list(completed['data'].get('models'))
        series_data = _list(completed['data'].get('series'))
        series_names: dict[str, str] = {
            _str(s.get('id'), ''): _str(s.get('name'), '') for s in series_data
        }

        for m in models_data:
            model_id: str = _str(m.get('id'))
            series_id: str = _str(m.get('series_id'))
            m_name = series_names.get(series_id, series_id)
            status: str = _str(m.get('status'), 'ready')
            print(f'  {model_id}  {m_name}  {series_id}  {status}')
    elif code != 0:
        if result.stderr.strip():
            print(f'error: {result.stderr.strip().splitlines()[-1]}')

    _print_footer(code, elapsed, result, 'models')
    return code


def _format_forecast_line(forecast: JSONObject, series_names: dict[str, str]) -> str:
    """Format a single forecast as a one-line summary."""
    fct_id: str = _str(forecast.get('id'))
    series_id: str = _str(forecast.get('series_id'))
    name = series_names.get(series_id, series_id)
    items = _list(forecast.get('items'))
    n_periods = len(items)

    if items:
        first_date = str(items[0].get('date', ''))[:7]
        last_date = str(items[-1].get('date', ''))[:7]
        date_str = f'{first_date} -> {last_date}'
    else:
        date_str = '-'

    return f'  {fct_id}  {name}  {n_periods} periods  {date_str}'


def format_forecast_line(f: JSONObject) -> str:
    """Format a forecast from D1 as a one-line summary."""
    import json

    fct_id: str = _str(f.get('id'))
    series_id: str = _str(f.get('series_id'))
    series_name: str = _str(f.get('series_name'), '')
    series_str = f'{series_name} ({series_id})' if series_name else series_id
    interval: str = _str(f.get('interval'), '')
    horizon: str = _str(f.get('horizon'), '')

    raw_items = f.get('items', '[]')
    items: list[JSONObject]
    if isinstance(raw_items, str):
        try:
            parsed: object = json.loads(raw_items)  # pyright: ignore[reportAny]
            items = (
                [cast(JSONObject, i) for i in cast(list[object], parsed) if isinstance(i, dict)]
                if isinstance(parsed, list)
                else []
            )
        except (json.JSONDecodeError, TypeError):
            items = []
    elif isinstance(raw_items, list):
        items = [cast(JSONObject, i) for i in cast(list[object], raw_items) if isinstance(i, dict)]
    else:
        items = []

    if items:
        first_date = str(items[0].get('date', ''))[:7]
        last_date = str(items[-1].get('date', ''))[:7]
        date_str = f'{first_date} -> {last_date}'
    else:
        date_str = '-'

    created_at = str(f.get('created_at', ''))[:16]
    parts = [f'  {fct_id}', series_str]
    if horizon and interval:
        parts.append(f'{horizon} {interval}s')
    parts.append(date_str)
    if created_at:
        parts.append(created_at)
    return '  '.join(parts)


def print_forecast_result(result: StreamResult, elapsed: float) -> int:
    """Print final result for forecast command. Returns exit code.

    Progress and per-series events are already printed by on_forecast_event.
    This prints the summary table from ForecastingCompleted and the footer.
    """
    code = result.exit_code if result.exit_code is not None else 1

    if result.errors:
        for err in result.errors:
            print(f'error: {err}')
        _print_footer(code, elapsed, result, 'forecast')
        return code

    completed = result.get_event('ForecastingCompleted')
    if completed is not None:
        forecasts_data = _list(completed['data'].get('forecasts'))
        series_data = _list(completed['data'].get('series'))
        series_names: dict[str, str] = {
            _str(s.get('id'), ''): _str(s.get('name'), '') for s in series_data
        }

        for f in forecasts_data:
            print(_format_forecast_line(f, series_names))
    elif code != 0:
        if result.stderr.strip():
            print(f'error: {result.stderr.strip().splitlines()[-1]}')

    _print_footer(code, elapsed, result, 'forecast')
    return code
