# PYTHON_ARGCOMPLETE_OK
from __future__ import annotations

import argparse
import curses
import json
import os
import shlex
import subprocess as sp
import sys
import time
from dataclasses import dataclass
from importlib.metadata import version
from multiprocessing import set_start_method
from pathlib import Path
from typing import Any, Callable

import argcomplete

from tjex import curses_helper, logging
from tjex.config import config as loaded_config
from tjex.config import load as load_config
from tjex.curses_helper import DummyRegion, KeyReader, Region, SubRegion, WindowRegion
from tjex.jq import (
    Jq,
    JqResult,
    append_filter,
    append_selector,
    key_to_selector,
    keys_to_selector,
    standalone_selector,
)
from tjex.json_table import TableCell, TableKey, Undefined
from tjex.logging import logger
from tjex.panel import Event, KeyBindings, KeyPress, StatusUpdate
from tjex.point import Point
from tjex.table_panel import TablePanel, TableState
from tjex.text_panel import TextEditPanel, TextPanel
from tjex.utils import TjexError, TmpFiles


def append_history(jq_cmd: str) -> StatusUpdate:
    skip = False
    cmd: list[str] = ["tjex"]
    for arg in sys.argv[1:]:
        if skip:
            skip = False
        else:
            if arg in {"-c", "--command"}:
                skip = True
            elif arg.startswith("-c") or arg.startswith("--command"):
                pass
            else:
                cmd.append(arg)
    cmd += ["--command", jq_cmd]
    cmd_str = " ".join(shlex.quote(arg) for arg in cmd)

    logger.debug(f"Trying to add to atuin history: {cmd_str}")
    result = sp.run(
        [
            "bash",
            "-c",
            loaded_config.append_history_command.format(shlex.quote(cmd_str)),
        ],
        capture_output=True,
    )
    if result.returncode:
        return StatusUpdate(result.stderr.decode("utf8"))
    return StatusUpdate("Added to atuin history.")


@dataclass
class Quit(Event):
    pass


def tjex(
    screen: Region,
    key_reader: Callable[[], str | None],
    screen_erase: Callable[[], None],
    screen_refresh: Callable[[], None],
    file: list[Path],
    command: str,
    config: Path,
    max_cell_width: int | None,
    slurp: bool,
) -> int:
    table = TablePanel[TableKey, TableCell](screen)
    prompt_head = TextEditPanel("> ")
    prompt = TextEditPanel(command)
    status = TextPanel("")
    status_detail_region = SubRegion(DummyRegion(), Point.ZERO, Point.ZERO)
    status_detail = TextPanel("", clear_first=True)
    status_detail.attr = curses.A_DIM
    panels = [table, prompt_head, prompt, status, status_detail]

    def resize(status_detail_height: None | int = None):
        nonlocal status_detail_region
        status_height = 1
        screen.resize()
        size = screen.size
        table.resize(SubRegion(screen, Point(0, 0), size - Point(3, 0)))
        prompt_head.resize(
            SubRegion(screen, Point(size.y - status_height - 1, 0), Point(1, 2))
        )
        prompt.resize(
            SubRegion(
                screen,
                Point(size.y - status_height - 1, 2),
                Point(1, size.x - 2),
            )
        )
        status.resize(
            SubRegion(
                screen,
                Point(size.y - status_height, 0),
                Point(status_height, size.x),
            )
        )
        if status_detail_height is None:
            status_detail_height = status_detail_region.height
        status_detail_region = SubRegion(
            screen,
            Point(size.y - status_height - 1 - status_detail_height, 0),
            Point(status_detail_height, size.x),
        )
        status_detail.resize(status_detail_region)

    jq = Jq(file, slurp)

    current_command: str = command
    table_cursor_history: dict[str, TableState] = {}

    def set_status(msg: str):
        logger.debug(msg)
        lines = msg.splitlines()
        status.content = "\n".join(lines[:1])
        if len(lines) <= 1:
            resize(status_detail_height=0)
            status_detail.content = ""
        else:
            resize(status_detail_height=len(lines))
            status_detail.content = "\n".join(lines)

    def update_jq_status(block: bool = False):
        nonlocal current_command
        match jq.status(block):
            case JqResult(msg, content):
                if msg == "...":
                    # If result is pending, don't clear previous error message
                    status.content = msg
                else:
                    set_status(msg)
                if content is not None and jq.command is not None:
                    table_cursor_history[current_command] = table.state
                    current_command = jq.command
                    table.update(content, table_cursor_history.get(current_command))
                return True
            case _:
                return False

    bindings: KeyBindings[None, Event | None] = KeyBindings()

    @bindings.add("C-g", "C-d")
    def quit(_: None):  # pyright: ignore[reportUnusedFunction]
        return Quit()

    @bindings.add("M-o", "C-o")
    def toggle_active(_: None):  # pyright: ignore[reportUnusedFunction]
        """Toggle active panel between prompt and table"""
        if active_cycle[0] == prompt:
            update_jq_status(block=True)  # pyright: ignore[reportUnusedCallResult]
        if active_cycle[0] != prompt or jq.latest_status.table is not None:
            active_cycle.append(active_cycle.pop(0))
            active_cycle[-1].set_active(False)
            active_cycle[0].set_active(True)

    _ = bindings.add("C-_", "ESC", name="undo")(lambda _: prompt.undo())
    _ = bindings.add("M-_", name="redo")(lambda _: prompt.redo())

    @bindings.add("M-\n")
    def add_to_history(_: None):  # pyright: ignore[reportUnusedFunction]
        """Append tjex call with current command to shell's history"""
        return append_history(prompt.content)

    @bindings.add("g", "\n")
    def reload(_: None):  # pyright: ignore[reportUnusedFunction]
        """Re-run the current filter."""
        jq.update(prompt.content, force=True)

    @table.bindings.add("M-w")
    def copy_content(_: Any):  # pyright: ignore[reportUnusedFunction]
        """Copy output of current command to clipboard"""
        loaded_config.do_copy(json.dumps(jq.run_plain(), ensure_ascii=False))
        return StatusUpdate("Copied.")

    @table.bindings.add("\n")
    def enter_cell(_: Any):  # pyright: ignore[reportUnusedFunction]
        """Enter highlighted cell by appending selector to jq prompt"""
        prompt.update(
            append_selector(prompt.content, keys_to_selector(*table.cell_keys))
        )

    @table.bindings.add("M-\n")
    def enter_row(_: Any):  # pyright: ignore[reportUnusedFunction]
        """Enter highlighted cell's row by appending selector to jq prompt"""
        prompt.update(append_selector(prompt.content, keys_to_selector(table.row_key)))

    @table.bindings.add("w")
    def copy_cell_content(_: Any):  # pyright: ignore[reportUnusedFunction]
        """Copy content of the current cell to clipboard.
        If content is a string, copy the plain value, not the json representation.
        """
        content = jq.run_plain(
            append_selector(jq.command or ".", keys_to_selector(*table.cell_keys) or "")
        )
        if isinstance(content, str):
            loaded_config.do_copy(content)
        else:
            loaded_config.do_copy(json.dumps(content, ensure_ascii=False))
        return StatusUpdate("Copied.")

    def prompt_append(command: str, cursor: TableState | None = None):
        new_prompt = append_filter(prompt.content, command)
        if cursor is not None:
            table_cursor_history[new_prompt] = cursor
        prompt.update(new_prompt)

    @table.bindings.add("E")
    def expand_row(_: Any):  # pyright: ignore[reportUnusedFunction]
        """Expand the selected row"""
        key = table.row_key
        if key == Undefined():
            raise TjexError("Not an array or object")
        prompt_append(f"expand({json.dumps(key, ensure_ascii=False)})", table.state)

    @table.bindings.add("e")
    def expand_col(_: Any):  # pyright: ignore[reportUnusedFunction]
        """Expand the selected column"""
        key = table.col_key
        if key == Undefined():
            raise TjexError("Not an array or object")
        prompt_append(f"map_values(expand({json.dumps(key, ensure_ascii=False)}))", table.state)

    @table.bindings.add("K")
    def delete_row(_: Any):  # pyright: ignore[reportUnusedFunction]
        """Delete the selected row"""
        key = table.row_key
        if key == Undefined():
            raise TjexError("Not an array or object")
        prompt_append(f"del({standalone_selector(key_to_selector(key))})", table.state)

    @table.bindings.add("k")
    def delete_col(_: Any):  # pyright: ignore[reportUnusedFunction]
        """Delete the selected column"""
        key = table.col_key
        if key == Undefined():
            raise TjexError("Not an array or object")
        prompt_append(
            f"map_values(del({standalone_selector(key_to_selector(key))}))", table.state
        )

    @table.bindings.add("m")
    def select_col(_: Any):  # pyright: ignore[reportUnusedFunction]
        """Enter the selected column"""
        key = table.col_key
        if key == Undefined():
            raise TjexError("Not an array or object")
        prompt_append(
            f"map_values({standalone_selector(key_to_selector(key))})",
            table.state.row_only,
        )

    @table.bindings.add("s")
    def sort_by_col(_: Any):  # pyright: ignore[reportUnusedFunction]
        """Sort rows by the selected column.
        Works only for arrays right now.
        """
        if not isinstance(table.row_key, int):
            raise TjexError("Not an array")
        key = table.col_key
        if key == Undefined():
            prompt.update(append_filter(prompt.content, f"sort"))
        else:
            prompt_append(
                f"sort_by({standalone_selector(key_to_selector(key))})",
                table.state.col_only,
            )

    load_config(
        config, {"global": bindings, "prompt": prompt.bindings, "table": table.bindings}
    )
    if max_cell_width:
        loaded_config.max_cell_width = max_cell_width

    resize()
    active_cycle = [table, prompt]
    if loaded_config.start_at_prompt:
        active_cycle = [prompt, table]
    active_cycle[0].set_active(True)
    jq.update(prompt.content)

    redraw = True

    while True:
        if (key := key_reader()) is not None:
            try:
                for event in active_cycle[0].handle_key(KeyPress(key)):
                    match bindings.handle_key(event, None):
                        case Quit():
                            return 0
                        case StatusUpdate(msg):
                            set_status(msg)
                        case KeyPress("KEY_RESIZE"):
                            resize()
                        case _:
                            pass
            except TjexError as e:
                set_status(e.msg)
            jq.update(prompt.content)
            redraw = True
            continue

        if update_jq_status() or redraw:
            screen_erase()
            for panel in panels:
                panel.draw()
            screen_refresh()
            redraw = False
            continue

        time.sleep(0.01)


def main():
    set_start_method("forkserver")
    parser = argparse.ArgumentParser(description="A tabular json explorer.")
    _ = parser.add_argument("--version", action="version", version=version("tjex"))
    _ = parser.add_argument("file", type=Path, nargs="*")
    _ = parser.add_argument("-c", "--command", default="")
    _ = parser.add_argument(
        "--config", type=Path, default=Path.home() / ".config" / "tjex" / "config.toml"
    )
    _ = parser.add_argument("--logfile", type=Path)
    _ = parser.add_argument("-w", "--max-cell-width", type=int)
    _ = parser.add_argument("-s", "--slurp", action="store_true")
    argcomplete.autocomplete(parser)
    args = parser.parse_args()
    logging.setup(args.logfile)

    with TmpFiles() as tmpfile:
        if not args.file:
            args.file = [tmpfile(sys.stdin.read())]
            os.close(0)
            sys.stdin = open("/dev/tty")
        for i in range(len(args.file)):
            if not args.file[i].is_file():
                args.file[i] = tmpfile(args.file[i].read_text())

        @curses.wrapper
        def result(scr: curses.window):
            _ = curses.curs_set(0)
            curses_helper.setup_plain_colors()
            return tjex(
                WindowRegion(scr),
                KeyReader(scr).get,
                scr.erase,
                scr.refresh,
                **{n: k for n, k in vars(args).items() if n not in {"logfile"}},
            )

    return result


if __name__ == "__main__":
    exit(main())
