"""Operaciones de escritura sobre celdas de un notebook Jupyter via colaboracion en tiempo real.

NO ejecuta celdas — solo modifica la estructura del notebook (append, insert, edit, delete, create, batch).
Usa jupyter_nbmodel_client para comunicarse con el servidor Jupyter via WebSocket.
Para crear notebooks usa la API REST de Jupyter (urllib PUT /api/contents).
"""

import asyncio
import json
import argparse
import sys
from urllib.error import URLError, HTTPError
from urllib.request import Request, urlopen

from jupyter_nbmodel_client import NbModelClient, get_jupyter_notebook_websocket_url


def _resolve_collab_username(server_url: str, token: str) -> str:
    """Resolve the display name of the active user in Jupyter collaboration."""
    headers = {"Accept": "application/json"}
    if token:
        headers["Authorization"] = f"token {token}"
    try:
        req = Request(f"{server_url}/api/me", headers=headers)
        with urlopen(req, timeout=5) as resp:
            me = json.loads(resp.read())
        identity = me.get("identity", {})
        return identity.get("display_name", "") or identity.get("username", "") or identity.get("name", "Anonymous")
    except (URLError, OSError, json.JSONDecodeError):
        return "Anonymous"


# ---------------------------------------------------------------------------
# Helpers internos
# ---------------------------------------------------------------------------


def _notebook_exists(notebook_path: str, server_url: str, token: str) -> bool:
    """Comprueba si un notebook existe en el servidor Jupyter via GET /api/contents.

    Usa GET con ?content=0 (metadata only). HEAD no es soportado universalmente
    (Jupyter 4.x devuelve 405 Method Not Allowed en /api/contents/{path}).
    """
    headers = {"Accept": "application/json"}
    if token:
        headers["Authorization"] = f"token {token}"
    check_url = f"{server_url}/api/contents/{notebook_path}?content=0"
    req = Request(check_url, headers=headers, method="GET")
    try:
        with urlopen(req, timeout=5):
            return True
    except HTTPError as e:
        if e.code == 404:
            return False
        raise


def _auto_create_notebook(notebook_path: str, server_url: str, token: str) -> bool:
    """Crea el notebook si no existe. Retorna True si fue creado."""
    if not _notebook_exists(notebook_path, server_url, token):
        jupyter_create_notebook(notebook_path, server_url=server_url, token=token)
        return True
    return False


# ---------------------------------------------------------------------------
# Helpers internos async
# ---------------------------------------------------------------------------


async def _append_cell(
    notebook_path: str,
    source: str,
    cell_type: str,
    server_url: str,
    token: str,
) -> dict:
    _auto_create_notebook(notebook_path, server_url, token)
    ws_url = get_jupyter_notebook_websocket_url(
        server_url=server_url,
        token=token,
        path=notebook_path,
    )
    username = _resolve_collab_username(server_url, token)
    async with NbModelClient(ws_url, username=username) as nb:
        if cell_type == "markdown":
            nb.add_markdown_cell(source)
        else:
            nb.add_code_cell(source)
        cell_index = len(nb) - 1
        await asyncio.sleep(2)
    return {
        "action": f"append_{cell_type}",
        "cell_index": cell_index,
        "notebook": notebook_path,
    }


async def _insert_cell(
    notebook_path: str,
    cell_index: int,
    source: str,
    cell_type: str,
    server_url: str,
    token: str,
) -> dict:
    ws_url = get_jupyter_notebook_websocket_url(
        server_url=server_url,
        token=token,
        path=notebook_path,
    )
    username = _resolve_collab_username(server_url, token)
    async with NbModelClient(ws_url, username=username) as nb:
        nb.insert_cell(cell_index, cell_type=cell_type, source=source)
        await asyncio.sleep(2)
    return {
        "action": "insert",
        "cell_index": cell_index,
        "cell_type": cell_type,
        "notebook": notebook_path,
    }


async def _edit_cell(
    notebook_path: str,
    cell_index: int,
    source: str,
    server_url: str,
    token: str,
) -> dict:
    ws_url = get_jupyter_notebook_websocket_url(
        server_url=server_url,
        token=token,
        path=notebook_path,
    )
    username = _resolve_collab_username(server_url, token)
    async with NbModelClient(ws_url, username=username) as nb:
        nb.set_cell_source(cell_index, source)
        await asyncio.sleep(2)
    return {
        "action": "edit",
        "cell_index": cell_index,
        "notebook": notebook_path,
    }


async def _delete_cell(
    notebook_path: str,
    cell_index: int,
    server_url: str,
    token: str,
) -> dict:
    ws_url = get_jupyter_notebook_websocket_url(
        server_url=server_url,
        token=token,
        path=notebook_path,
    )
    username = _resolve_collab_username(server_url, token)
    async with NbModelClient(ws_url, username=username) as nb:
        nb.delete_cell(cell_index)
        await asyncio.sleep(2)
    return {
        "action": "delete",
        "cell_index": cell_index,
        "notebook": notebook_path,
    }


async def _batch_write(
    notebook_path: str,
    cells: list,
    server_url: str,
    token: str,
) -> dict:
    """Anade multiples celdas en una sola conexion WebSocket."""
    _auto_create_notebook(notebook_path, server_url, token)
    ws_url = get_jupyter_notebook_websocket_url(
        server_url=server_url,
        token=token,
        path=notebook_path,
    )
    username = _resolve_collab_username(server_url, token)
    async with NbModelClient(ws_url, username=username) as nb:
        for cell in cells:
            cell_type = cell.get("type", "code")
            source = cell.get("source", "")
            if cell_type == "markdown":
                nb.add_markdown_cell(source)
            else:
                nb.add_code_cell(source)
        await asyncio.sleep(2)
    return {
        "action": "batch",
        "cells_added": len(cells),
        "notebook": notebook_path,
    }


# ---------------------------------------------------------------------------
# API publica sincrona
# ---------------------------------------------------------------------------


def jupyter_append_code(
    notebook_path: str,
    source: str,
    server_url: str = "http://localhost:8888",
    token: str = "",
) -> dict:
    """Anade una celda de codigo al final del notebook.

    Args:
        notebook_path: Ruta relativa al notebook dentro del servidor Jupyter.
        source: Codigo fuente de la celda.
        server_url: URL base del servidor Jupyter.
        token: Token de autenticacion del servidor Jupyter.

    Returns:
        dict con action, cell_index y notebook.
    """
    return asyncio.run(_append_cell(notebook_path, source, "code", server_url, token))


def jupyter_append_markdown(
    notebook_path: str,
    source: str,
    server_url: str = "http://localhost:8888",
    token: str = "",
) -> dict:
    """Anade una celda markdown al final del notebook.

    Args:
        notebook_path: Ruta relativa al notebook dentro del servidor Jupyter.
        source: Contenido markdown de la celda.
        server_url: URL base del servidor Jupyter.
        token: Token de autenticacion del servidor Jupyter.

    Returns:
        dict con action, cell_index y notebook.
    """
    return asyncio.run(
        _append_cell(notebook_path, source, "markdown", server_url, token)
    )


def jupyter_insert_cell(
    notebook_path: str,
    cell_index: int,
    source: str,
    cell_type: str = "code",
    server_url: str = "http://localhost:8888",
    token: str = "",
) -> dict:
    """Inserta una celda en una posicion especifica del notebook.

    Args:
        notebook_path: Ruta relativa al notebook dentro del servidor Jupyter.
        cell_index: Indice donde insertar (0 = primer posicion).
        source: Contenido de la celda.
        cell_type: Tipo de celda: "code" o "markdown".
        server_url: URL base del servidor Jupyter.
        token: Token de autenticacion del servidor Jupyter.

    Returns:
        dict con action, cell_index, cell_type y notebook.
    """
    return asyncio.run(
        _insert_cell(notebook_path, cell_index, source, cell_type, server_url, token)
    )


def jupyter_edit_cell(
    notebook_path: str,
    cell_index: int,
    source: str,
    server_url: str = "http://localhost:8888",
    token: str = "",
) -> dict:
    """Sobrescribe el contenido de una celda existente.

    Args:
        notebook_path: Ruta relativa al notebook dentro del servidor Jupyter.
        cell_index: Indice de la celda a editar (0-based).
        source: Nuevo contenido de la celda.
        server_url: URL base del servidor Jupyter.
        token: Token de autenticacion del servidor Jupyter.

    Returns:
        dict con action, cell_index y notebook.
    """
    return asyncio.run(
        _edit_cell(notebook_path, cell_index, source, server_url, token)
    )


def jupyter_delete_cell(
    notebook_path: str,
    cell_index: int,
    server_url: str = "http://localhost:8888",
    token: str = "",
) -> dict:
    """Elimina una celda del notebook.

    Args:
        notebook_path: Ruta relativa al notebook dentro del servidor Jupyter.
        cell_index: Indice de la celda a eliminar (0-based).
        server_url: URL base del servidor Jupyter.
        token: Token de autenticacion del servidor Jupyter.

    Returns:
        dict con action, cell_index y notebook.
    """
    return asyncio.run(
        _delete_cell(notebook_path, cell_index, server_url, token)
    )


def jupyter_create_notebook(
    notebook_path: str,
    kernel_name: str = "python3",
    server_url: str = "http://localhost:8888",
    token: str = "",
    force: bool = False,
) -> dict:
    """Crea un notebook vacio (nbformat 4) via la API REST de Jupyter.

    Usa PUT /api/contents/{path} con type: notebook. Si el notebook ya existe
    y force=False, lanza un error en vez de sobreescribirlo.

    Args:
        notebook_path: Ruta relativa al notebook dentro del servidor Jupyter.
        kernel_name: Nombre del kernel a asociar (default: python3).
        server_url: URL base del servidor Jupyter.
        token: Token de autenticacion del servidor Jupyter.
        force: Si True, sobreescribe el notebook si ya existe.

    Returns:
        dict con action, notebook y created.
    """
    headers = {
        "Content-Type": "application/json",
        "Accept": "application/json",
    }
    if token:
        headers["Authorization"] = f"token {token}"

    # Verificar si ya existe (GET con content=0; HEAD no esta soportado en Jupyter 4.x)
    check_url = f"{server_url}/api/contents/{notebook_path}"
    check_req = Request(f"{check_url}?content=0", headers=headers, method="GET")
    already_exists = False
    try:
        with urlopen(check_req, timeout=5):
            already_exists = True
    except HTTPError as e:
        if e.code != 404:
            raise
    except URLError:
        raise

    if already_exists and not force:
        raise FileExistsError(
            f"Notebook ya existe: {notebook_path}. Usa force=True para sobreescribir."
        )

    kernel_display = {
        "python3": "Python 3 (ipykernel)",
        "python": "Python 3",
    }.get(kernel_name, kernel_name)

    notebook_content = {
        "nbformat": 4,
        "nbformat_minor": 5,
        "metadata": {
            "kernelspec": {
                "name": kernel_name,
                "display_name": kernel_display,
                "language": "python",
            },
            "language_info": {
                "name": "python",
            },
        },
        "cells": [],
    }

    body = json.dumps({
        "type": "notebook",
        "content": notebook_content,
    }).encode("utf-8")

    put_req = Request(check_url, data=body, headers=headers, method="PUT")
    with urlopen(put_req, timeout=10) as resp:
        resp.read()

    return {
        "action": "create",
        "notebook": notebook_path,
        "created": True,
    }


def jupyter_batch_write(
    notebook_path: str,
    cells: list,
    server_url: str = "http://localhost:8888",
    token: str = "",
) -> dict:
    """Anade multiples celdas al notebook en una sola conexion WebSocket.

    Mucho mas eficiente que llamar append-code/append-markdown N veces porque
    abre una unica conexion WebSocket y hace un solo sleep de sincronizacion
    al final de todas las inserciones.

    Args:
        notebook_path: Ruta relativa al notebook dentro del servidor Jupyter.
        cells: Lista de dicts con claves "type" ("code"|"markdown") y "source" (str).
        server_url: URL base del servidor Jupyter.
        token: Token de autenticacion del servidor Jupyter.

    Returns:
        dict con action, cells_added y notebook.
    """
    return asyncio.run(_batch_write(notebook_path, cells, server_url, token))


# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------


def _build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        prog="jupyter_write",
        description="Operaciones de escritura sobre celdas de un notebook Jupyter.",
    )
    sub = parser.add_subparsers(dest="command", required=True)

    # Argumentos comunes
    def add_common(p: argparse.ArgumentParser) -> None:
        p.add_argument("--server", default="http://localhost:8888", help="URL del servidor Jupyter")
        p.add_argument("--token", default="", help="Token de autenticacion")

    # append-code
    p_ac = sub.add_parser("append-code", help="Anade celda de codigo al final")
    p_ac.add_argument("notebook", help="Ruta del notebook")
    p_ac.add_argument("source", help="Codigo fuente")
    add_common(p_ac)

    # append-markdown
    p_am = sub.add_parser("append-markdown", help="Anade celda markdown al final")
    p_am.add_argument("notebook", help="Ruta del notebook")
    p_am.add_argument("source", help="Contenido markdown")
    add_common(p_am)

    # insert
    p_ins = sub.add_parser("insert", help="Inserta celda en posicion especifica")
    p_ins.add_argument("notebook", help="Ruta del notebook")
    p_ins.add_argument("index", type=int, help="Indice de insercion (0-based)")
    p_ins.add_argument("source", help="Contenido de la celda")
    p_ins.add_argument("--type", dest="cell_type", choices=["code", "markdown"], default="code")
    add_common(p_ins)

    # edit
    p_ed = sub.add_parser("edit", help="Sobrescribe el contenido de una celda")
    p_ed.add_argument("notebook", help="Ruta del notebook")
    p_ed.add_argument("index", type=int, help="Indice de la celda (0-based)")
    p_ed.add_argument("source", help="Nuevo contenido")
    add_common(p_ed)

    # delete
    p_del = sub.add_parser("delete", help="Elimina una celda")
    p_del.add_argument("notebook", help="Ruta del notebook")
    p_del.add_argument("index", type=int, help="Indice de la celda (0-based)")
    add_common(p_del)

    # create
    p_cr = sub.add_parser("create", help="Crea un notebook vacio via API REST")
    p_cr.add_argument("notebook", help="Ruta del notebook a crear")
    p_cr.add_argument("--kernel", default="python3", help="Nombre del kernel (default: python3)")
    p_cr.add_argument("--force", action="store_true", help="Sobreescribir si ya existe")
    add_common(p_cr)

    # batch
    p_bt = sub.add_parser("batch", help="Anade multiples celdas en una sola conexion WebSocket")
    p_bt.add_argument("notebook", help="Ruta del notebook")
    p_bt.add_argument(
        "--from",
        dest="cells_source",
        default="-",
        metavar="FILE",
        help="Archivo JSON con las celdas, o '-' para leer de stdin (default: -)",
    )
    add_common(p_bt)

    return parser


def main() -> None:
    parser = _build_parser()
    args = parser.parse_args()

    if args.command == "append-code":
        result = jupyter_append_code(args.notebook, args.source, args.server, args.token)
    elif args.command == "append-markdown":
        result = jupyter_append_markdown(args.notebook, args.source, args.server, args.token)
    elif args.command == "insert":
        result = jupyter_insert_cell(
            args.notebook, args.index, args.source, args.cell_type, args.server, args.token
        )
    elif args.command == "edit":
        result = jupyter_edit_cell(args.notebook, args.index, args.source, args.server, args.token)
    elif args.command == "delete":
        result = jupyter_delete_cell(args.notebook, args.index, args.server, args.token)
    elif args.command == "create":
        result = jupyter_create_notebook(
            args.notebook,
            kernel_name=args.kernel,
            server_url=args.server,
            token=args.token,
            force=args.force,
        )
    elif args.command == "batch":
        if args.cells_source == "-":
            raw = sys.stdin.read()
        else:
            with open(args.cells_source, "r", encoding="utf-8") as f:
                raw = f.read()
        cells = json.loads(raw)
        result = jupyter_batch_write(args.notebook, cells, args.server, args.token)
    else:
        parser.print_help()
        return

    print(json.dumps(result, indent=2))


if __name__ == "__main__":
    main()
