"""HTTP entry point — bridges run_managed to the C HTTP server.

C calls dispatch_request(body_json) for every POST /exec.
This function always returns valid JSON. C never builds JSON.

Two request shapes:
  {"code": "..."}                                        → exec_and_respond
  {"control": "revert"|"cleanup", ...}                   → handle_control
"""

import io
import json
import sys
import traceback

from shortcut_xl._com import xl_app
from shortcut_xl._diff_highlight import DiffHighlightStore
from shortcut_xl._managed import EditModeError, run_managed
from shortcut_xl._threading import _run_on_main
from shortcut_xl._tracking import DirtyCell
from shortcut_xl.api.categorize import com_error_to_str
from shortcut_xl.api.utils.helpers import address_to_index, build_cell_ref

_MAX_DIFF_JSON_CELLS = 1000
# Single store instance for the lifetime of the server process.
# Owned here (not in _diff_highlight) so tests can create isolated instances.
_store = DiffHighlightStore()


def _format_cell_value(v):
    """Format a cell value for JSON display."""
    if v is None:
        return 'empty'
    if isinstance(v, bool):
        return 'TRUE' if v else 'FALSE'
    if isinstance(v, int):
        # COM error codes are large negative ints — display as error string
        err = com_error_to_str(v)
        if err is not None:
            return err
        return v
    if isinstance(v, str):
        return 'empty' if len(v) == 0 else v
    try:
        f = float(v)
        # Only convert to int if it fits exactly (avoids precision loss for large floats)
        i = int(f)
        return i if f == i and abs(i) <= 2**53 else round(f, 10)
    except (TypeError, ValueError):
        return str(v)


def _cell_sort_key(c: DirtyCell):
    """Sort key: (sheet, row, col) using address_to_index."""
    addr = c.get('address', '')
    try:
        row, col = address_to_index(addr)
    except (ValueError, AttributeError):
        row, col = 0, 0
    return (c.get('sheet', ''), row, col)


def _sort_dirty(dirty: list[DirtyCell]) -> list[DirtyCell]:
    """Sort dirty cells by (sheet, row, column) for row-wise display.

    Applied once before both _build_diff and highlight_changes so the
    entire pipeline sees a consistent order.
    """
    return sorted(dirty, key=_cell_sort_key)


def _build_diff(dirty: list[DirtyCell]):
    """Build structured diff payload for the TUI approval component.

    Expects *dirty* to be pre-sorted by _sort_dirty. One row per cell.
    """
    cells = []
    for c in dirty[:_MAX_DIFF_JSON_CELLS]:
        entry = {
            'address': build_cell_ref(c.get('workbook'), c.get('sheet', ''), c.get('address', '')),
            'before': _format_cell_value(c.get('oldValue')),
            'after': _format_cell_value(c.get('value')),
        }
        if c.get('oldFormula'):
            entry['oldFormula'] = c.get('oldFormula')
        if c.get('formula'):
            entry['formula'] = c.get('formula')
        cells.append(entry)
    return {'cells': cells, 'total': len(dirty)}


def _json_default(obj):
    """Fallback serializer for non-standard types (e.g. datetime)."""
    try:
        return str(obj)
    except Exception:
        return None


def dispatch_request(body_json):
    """Single entry point for all POST /exec requests.

    Always returns a valid JSON string. C sends this verbatim.
    """
    try:
        request = json.loads(body_json)
    except (json.JSONDecodeError, TypeError) as e:
        return json.dumps({'ok': False, 'error': f'Invalid JSON: {e}'})

    if not isinstance(request, dict):
        return json.dumps({'ok': False, 'error': 'request must be a JSON object'})

    if 'code' in request:
        return exec_and_respond(request['code'], auto_approve=request.get('autoApprove', False))
    elif 'control' in request:
        return handle_control(request)
    else:
        return json.dumps({'ok': False, 'error': "request must have 'code' or 'control' field"})


def exec_and_respond(code_str, *, auto_approve=False):
    """Execute user code and return a JSON response string.

    Owns stdout/stderr capture, calls run_managed (pure), applies
    CF highlights, and builds the response. Always returns valid JSON.

    When *auto_approve* is True the caller already knows it will accept
    any changes, so we skip CF highlighting entirely — no highlights to
    apply, no revert data to store, no cleanup round-trip needed.
    """
    sio = io.StringIO()
    old_out, old_err = sys.stdout, sys.stderr
    sys.stdout = sys.stderr = sio
    try:
        dirty = run_managed(code_str)
        output = sio.getvalue()

        response = {'ok': True, 'output': output}
        if dirty:
            dirty = _sort_dirty(dirty)
            response['diff'] = _build_diff(dirty)
            # Only apply CF highlights when the user may review changes.
            if not auto_approve:
                cf_tx_id = _run_on_main(lambda: _store.highlight_changes(xl_app(), dirty))
                response['cfTxId'] = cf_tx_id
        return json.dumps(response, default=_json_default)
    except EditModeError as e:
        output = sio.getvalue()
        error = output or str(e) or 'Excel is in cell edit mode. Exit edit mode in Excel and retry.'
        return json.dumps({'ok': False, 'editMode': True, 'error': error})
    except Exception:
        output = sio.getvalue()
        tb = traceback.format_exc()
        response = {'ok': False, 'error': tb}
        if output:
            response['output'] = output
        return json.dumps(response, default=_json_default)
    finally:
        sys.stdout, sys.stderr = old_out, old_err


def handle_control(request):
    """Handle control commands (revert, cleanup).

    Revert data is stored server-side in _diff_highlight, keyed by cfTxId.
    TS only sends the ID — no dirty cell data round-trips through HTTP.
    """
    action = request.get('control')
    cf_tx_id = request.get('cfTxId')

    if action == 'revert':
        if cf_tx_id is None:
            return json.dumps({'ok': False, 'error': 'revert requires cfTxId'})
        n = _run_on_main(lambda: _store.revert_by_tx(xl_app(), cf_tx_id))
        if n == 0:
            return json.dumps(
                {'ok': True, 'output': 'Nothing to revert (transaction not found or already cleaned up).'}
            )
        return json.dumps({'ok': True, 'output': f'Reverted {n} cell{"s" if n != 1 else ""}.'})

    elif action == 'cleanup':
        if cf_tx_id is not None:
            _run_on_main(lambda: _store.cleanup_by_tx(cf_tx_id))
        return json.dumps({'ok': True, 'output': ''})

    else:
        return json.dumps({'ok': False, 'error': f'Unknown control action: {action}'})
