#
#   Imandra Inc.
#
#   codelogician/commands/server/cmd_sketches.py
#

from __future__ import annotations

import json
import urllib.parse
from typing import Any

import httpx
import typer

app = typer.Typer(name='sketches', help='CLI for CodeLogician sketches endpoints.')


# -------------------------
# Helpers
# -------------------------


def _print_json(data: Any) -> None:
    typer.echo(json.dumps(data, indent=2, ensure_ascii=False))


def _handle_http_error(resp: httpx.Response) -> None:
    try:
        payload = resp.json()
    except Exception:
        payload = None

    if isinstance(payload, dict) and 'detail' in payload:
        raise typer.BadParameter(f'HTTP {resp.status_code}: {payload["detail"]}')
    raise typer.BadParameter(f'HTTP {resp.status_code}: {resp.text}')


def _get(
    base_url: str,
    path: str,
    *,
    params: dict[str, Any] | None = None,
    timeout: float,
) -> Any:
    url = base_url.rstrip('/') + path
    with httpx.Client(timeout=timeout) as client:
        resp = client.get(url, params=params)
    if resp.status_code >= 400:
        _handle_http_error(resp)
    return resp.json()


def _post(
    base_url: str,
    path: str,
    *,
    params: dict[str, Any] | None = None,
    json_body: Any | None = None,
    timeout: float,
) -> Any:
    url = base_url.rstrip('/') + path
    with httpx.Client(timeout=timeout) as client:
        resp = client.post(url, params=params, json=json_body)
    if resp.status_code >= 400:
        _handle_http_error(resp)

    ctype = resp.headers.get('content-type', '')
    if 'application/json' in ctype:
        return resp.json()
    return resp.text


def _cfg(ctx: typer.Context) -> dict[str, Any]:
    root = ctx.find_root()
    assert isinstance(root.obj, dict)
    return root.obj


# -------------------------
# Commands
# -------------------------


@app.command('search')
def search_sketches(
    ctx: typer.Context,
    query: str = typer.Argument('', help='Search query string.'),
) -> None:
    """GET /sketches/search?query=..."""
    cfg = _cfg(ctx)
    data = _get(
        cfg['base_url'],
        '/sketches/search',
        params={'query': query},
        timeout=cfg['timeout'],
    )

    if cfg['json_out']:
        _print_json(data)
    else:
        if isinstance(data, list):
            for item in data:
                typer.echo(str(item))
        else:
            typer.echo(str(data))


@app.command('list')
def list_sketches(ctx: typer.Context) -> None:
    """GET /sketches/list"""
    cfg = _cfg(ctx)
    data = _get(cfg['base_url'], '/sketches/list', timeout=cfg['timeout'])

    if cfg['json_out']:
        _print_json(data)
        return

    # Expected: dict[str, list[dict[str, str]]]
    if isinstance(data, dict):
        for strat_id, items in data.items():
            typer.echo(f'Strategy {strat_id}:')
            if isinstance(items, list):
                for it in items:
                    if isinstance(it, dict):
                        sid = it.get('sketch_id', '<unknown>')
                        anchor = it.get(
                            'anchor_model', it.get('anchormodel', '<unknown>')
                        )
                        typer.echo(f'  - {sid} (anchor: {anchor})')
                    else:
                        typer.echo(f'  - {it}')
            else:
                typer.echo(f'  {items}')
    else:
        typer.echo(str(data))


@app.command('create')
def create_sketch(
    ctx: typer.Context,
    anchor_model_path: str = typer.Argument(..., help='Anchor model path.'),
) -> None:
    """
    POST /sketches/create

    Note: endpoint signature is `create_sketch(anchor_model_path: str)` with no explicit
    Body/Query annotation. In FastAPI, this is typically treated as a query parameter.
    """
    cfg = _cfg(ctx)
    data = _post(
        cfg['base_url'],
        '/sketches/create',
        params={'anchor_model_path': anchor_model_path},
        timeout=cfg['timeout'],
    )
    _print_json(data) if cfg['json_out'] else typer.echo(str(data))


@app.command('try-change')
def try_change(
    ctx: typer.Context,
    sketch_id: str = typer.Argument(..., help='Sketch ID.'),
    change_json: str = typer.Option(
        ...,
        '--change-json',
        help='JSON payload for SketchChange (request body).',
    ),
    commit_on_success: bool = typer.Option(
        True,
        '--commit/--no-commit',
        help='Whether to commit the change automatically on success (default: commit).',
    ),
) -> None:
    """
    POST /sketches/{sketch_id}/try_change?commit_on_success=...

    Body: SketchChange JSON.
    """
    cfg = _cfg(ctx)

    try:
        change_obj = json.loads(change_json)
    except json.JSONDecodeError as e:
        raise typer.BadParameter(f'--change-json must be valid JSON: {e}') from e

    sid = urllib.parse.quote(sketch_id, safe='')
    data = _post(
        cfg['base_url'],
        f'/sketches/{sid}/try_change',
        params={'commit_on_success': commit_on_success},
        json_body=change_obj,
        timeout=cfg['timeout'],
    )
    _print_json(data) if cfg['json_out'] else typer.echo(str(data))


@app.command('change')
def apply_change(
    ctx: typer.Context,
    sketch_id: str = typer.Argument(..., help='Sketch ID.'),
    change_json: str = typer.Option(
        ...,
        '--change-json',
        help='JSON payload for one of SketchChgSetModel | SketchChgInsertDef | SketchChgModifyDef | SketchChgDeleteDef.',
    ),
) -> None:
    """POST /sketches/{sketch_id}/change (body: SketchChg*)"""
    cfg = _cfg(ctx)

    try:
        change_obj = json.loads(change_json)
    except json.JSONDecodeError as e:
        raise typer.BadParameter(f'--change-json must be valid JSON: {e}') from e

    sid = urllib.parse.quote(sketch_id, safe='')
    data = _post(
        cfg['base_url'],
        f'/sketches/{sid}/change',
        json_body=change_obj,
        timeout=cfg['timeout'],
    )
    _print_json(data) if cfg['json_out'] else typer.echo(str(data))


@app.command('rollback')
def rollback(
    ctx: typer.Context,
    sketch_id: str = typer.Argument(..., help='Sketch ID.'),
    target_state_id: int | None = typer.Option(
        None,
        '--target-state-id',
        help='Rollback to a specific state id. If omitted, roll back the last change.',
    ),
) -> None:
    """POST /sketches/{sketch_id}/rollback?target_state_id=..."""
    cfg = _cfg(ctx)

    sid = urllib.parse.quote(sketch_id, safe='')
    params = (
        {'target_state_id': target_state_id} if target_state_id is not None else None
    )
    data = _post(
        cfg['base_url'],
        f'/sketches/{sid}/rollback',
        params=params,
        timeout=cfg['timeout'],
    )
    _print_json(data) if cfg['json_out'] else typer.echo(str(data))


@app.command('state')
def latest_state(
    ctx: typer.Context,
    sketch_id: str = typer.Argument(..., help='Sketch ID.'),
) -> None:
    """
    POST /sketches/{sketch_id}/state

    Note: current server endpoint returns 'OK' (placeholder).
    """
    cfg = _cfg(ctx)

    sid = urllib.parse.quote(sketch_id, safe='')
    data = _post(cfg['base_url'], f'/sketches/{sid}/state', timeout=cfg['timeout'])
    if cfg['json_out'] and isinstance(data, (dict, list)):
        _print_json(data)
    else:
        typer.echo(str(data))


if __name__ == '__main__':
    app()
