from __future__ import annotations

import re
import sys
from pathlib import Path
from typing import Any, Dict, List, Sequence, Tuple

from SYS.cmdlet_spec import Cmdlet, CmdletArg, SharedArgs, parse_cmdlet_args
from SYS.logger import log
from SYS.result_table import Column, Table
from SYS.rich_display import stdout_console


_NUMERIC_NAMESPACE_HINTS = {
    "track",
    "disk",
    "disc",
    "episode",
    "season",
    "chapter",
    "volume",
    "part",
}
_WINDOWS_RESERVED_NAMES = {
    "con",
    "prn",
    "aux",
    "nul",
    *(f"com{i}" for i in range(1, 10)),
    *(f"lpt{i}" for i in range(1, 10)),
}
_ILLEGAL_FILENAME_CHARS_RE = re.compile(r'[<>:"/\\|?*]')


def _normalize_bool(value: Any) -> bool:
    text = str(value or "").strip().lower()
    return text in {"1", "true", "yes", "on", "y"}


def _parse_table_query(query: Any) -> Dict[str, str]:
    fields: Dict[str, str] = {}
    raw = str(query or "").strip()
    if not raw:
        return fields

    for chunk in re.split(r"[;,]+", raw):
        part = str(chunk or "").strip()
        if not part:
            continue
        sep_index = part.find(":")
        if sep_index < 0:
            sep_index = part.find("=")
        if sep_index <= 0:
            continue
        key = part[:sep_index].strip().lower()
        value = part[sep_index + 1 :].strip().strip('"').strip("'")
        if key:
            fields[key] = value
    return fields


def _active_table_bundle(ctx: Any) -> Tuple[Any, str]:
    display_table = ctx.get_display_table() if hasattr(ctx, "get_display_table") else None
    if display_table is not None:
        return display_table, "display"

    current_stage_table = ctx.get_current_stage_table() if hasattr(ctx, "get_current_stage_table") else None
    if current_stage_table is not None:
        return current_stage_table, "stage"

    last_result_table = ctx.get_last_result_table() if hasattr(ctx, "get_last_result_table") else None
    if last_result_table is not None:
        return last_result_table, "last"

    return None, ""


def _clone_table(source: Any) -> Any:
    if source is None or not isinstance(source, Table):
        return source

    cloned = source.copy_with_title(str(getattr(source, "title", "") or ""))
    for source_row in getattr(source, "rows", []) or []:
        row = cloned.add_row()
        row.columns = [
            Column(col.name, col.value, getattr(col, "width", None))
            for col in getattr(source_row, "columns", []) or []
        ]
        row.selection_args = list(getattr(source_row, "selection_args", []) or []) or None
        row.selection_action = list(getattr(source_row, "selection_action", []) or []) or None
        row.source_index = getattr(source_row, "source_index", None)
        row.payload = getattr(source_row, "payload", None)
    return cloned


def _column_sort_key(value: Any, *, numeric: bool = False) -> Tuple[int, Any, str]:
    text = str(value or "").strip()
    if not text:
        return (1, float("inf") if numeric else "", "")
    if numeric:
        match = re.search(r"-?\d+(?:\.\d+)?", text)
        if match:
            try:
                return (0, float(match.group(0)), text.casefold())
            except Exception:
                pass
        return (0, float("inf"), text.casefold())
    return (0, text.casefold(), text.casefold())


def _sort_by_column(table: Any, column_name: str, *, numeric: bool = False, reverse: bool = False) -> None:
    if table is None or not hasattr(table, "rows"):
        return

    wanted = str(column_name or "").strip().lower()
    if not wanted:
        return

    if wanted in {"title", "name"} and hasattr(table, "sort_by_title"):
        table.sort_by_title()
        if reverse and hasattr(table, "rows"):
            table.rows.reverse()
        return

    if wanted == "tag" and hasattr(table, "sort_by_title"):
        table.rows.sort(
            key=lambda row: _column_sort_key(row.get_column("Tag"), numeric=numeric),
            reverse=bool(reverse),
        )
        return

    table.rows.sort(
        key=lambda row: _column_sort_key(row.get_column(column_name), numeric=numeric),
        reverse=bool(reverse),
    )


def _reorder_items_from_table(table: Any, items: List[Any]) -> List[Any]:
    if not items or table is None or not hasattr(table, "rows"):
        return list(items or [])

    payloads: List[Any] = []
    for row in getattr(table, "rows", []) or []:
        payload = getattr(row, "payload", None)
        if payload is None:
            payloads = []
            break
        payloads.append(payload)
    if payloads and len(payloads) == len(getattr(table, "rows", []) or []):
        return payloads

    reordered: List[Any] = []
    for row in getattr(table, "rows", []) or []:
        source_index = getattr(row, "source_index", None)
        if isinstance(source_index, int) and 0 <= source_index < len(items):
            reordered.append(items[source_index])

    if reordered and len(reordered) == len(getattr(table, "rows", []) or []):
        return reordered
    return list(items or [])


def _render_table(table: Any) -> int:
    if table is None:
        log("No active result table", file=sys.stderr)
        return 1

    try:
        setattr(table, "_rendered_by_cmdlet", True)
    except Exception:
        pass

    try:
        if hasattr(table, "to_rich"):
            stdout_console().print(table.to_rich())
            return 0
    except Exception as exc:
        log(f"Failed to render table: {exc}", file=sys.stderr)
        return 1

    try:
        print(table)
        return 0
    except Exception as exc:
        log(f"Failed to print table: {exc}", file=sys.stderr)
        return 1


def _sanitize_filename_base(text: str) -> str:
    s = str(text or "").strip()
    if not s:
        return "table"

    s = _ILLEGAL_FILENAME_CHARS_RE.sub(" ", s)
    s = "".join(ch for ch in s if ch.isprintable())
    s = " ".join(s.split()).strip()
    s = s.rstrip(" .")

    if not s:
        s = "table"
    if s.lower() in _WINDOWS_RESERVED_NAMES:
        s = f"_{s}"
    if len(s) > 200:
        s = s[:200].rstrip(" .")
    return s or "table"


def _resolve_output_path(path_arg: str, *, table_title: str) -> Path:
    raw = str(path_arg or "").strip()
    if not raw:
        raise ValueError("-path is required")

    ends_with_sep = raw.endswith(("/", "\\"))
    target = Path(raw)

    if target.exists() and target.is_dir():
        return target / f"{_sanitize_filename_base(table_title)}.svg"

    if (ends_with_sep or not target.suffix) and not target.exists():
        target.mkdir(parents=True, exist_ok=True)
        return target / f"{_sanitize_filename_base(table_title)}.svg"

    if not target.suffix:
        target.parent.mkdir(parents=True, exist_ok=True)
        return target.with_suffix(".svg")
    if target.suffix.lower() != ".svg":
        return target.with_suffix(".svg")
    return target


def _export_table_svg(table: Any, path_arg: str) -> int:
    if table is None:
        log("No table available to export", file=sys.stderr)
        return 1

    title_text = str(getattr(table, "title", None) or "table")

    try:
        out_path = _resolve_output_path(path_arg, table_title=title_text)
        out_path.parent.mkdir(parents=True, exist_ok=True)

        from rich.console import Console

        console = Console(record=True)
        renderable = table.to_rich() if hasattr(table, "to_rich") else table
        console.print(renderable)
        console.save_svg(str(out_path))
        log(f"Saved table SVG: {out_path}")
        return 0
    except Exception as exc:
        log(f"Failed to save table SVG: {type(exc).__name__}: {exc}", file=sys.stderr)
        return 1


def _apply_table_sort(table: Any, *, sort_column: str, query_text: str) -> int:
    query_fields = _parse_table_query(query_text)
    wanted_column = str(sort_column or query_fields.get("sort") or "").strip()
    namespace = str(query_fields.get("namespace") or "").strip().rstrip(":")
    order = str(query_fields.get("format") or query_fields.get("order") or "asc").strip().lower()
    reverse = order in {"desc", "descending", "reverse", "z-a"}

    numeric_field = query_fields.get("numeric")
    if numeric_field is not None:
        numeric = _normalize_bool(numeric_field)
    else:
        numeric = namespace.casefold() in _NUMERIC_NAMESPACE_HINTS

    if not wanted_column and namespace:
        wanted_column = "tag"
    if not wanted_column:
        wanted_column = "title"

    try:
        if str(wanted_column).strip().lower() == "tag" and namespace:
            if not hasattr(table, "sort_by_tag_namespace"):
                log("Current table does not support namespace sorting", file=sys.stderr)
                return 1
            table.sort_by_tag_namespace(namespace, numeric=numeric, reverse=reverse)
        else:
            _sort_by_column(table, wanted_column, numeric=numeric, reverse=reverse)
    except Exception as exc:
        log(f"Failed to sort table: {exc}", file=sys.stderr)
        return 1

    if hasattr(table, "_perseverance"):
        try:
            table._perseverance(True)
        except Exception:
            pass
    return 0


def _run(piped_result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
    _ = piped_result, config

    try:
        from SYS import pipeline as ctx
    except Exception as exc:
        log(f"Failed to import pipeline context: {exc}")
        return 1

    parsed = parse_cmdlet_args(args, CMDLET)
    sort_column = str(parsed.get("sort") or "").strip()
    query_text = str(parsed.get("query") or "").strip()
    debug_mode = bool(parsed.get("debug", False))
    print_mode = bool(parsed.get("print", False))
    path_arg = str(parsed.get("path") or "").strip()

    active_table, _table_kind = _active_table_bundle(ctx)

    if print_mode or path_arg:
        if not path_arg:
            log("Missing required -path for table export", file=sys.stderr)
            return 1
        return _export_table_svg(active_table, path_arg)

    if not debug_mode and not sort_column and not query_text:
        return _render_table(active_table)

    if not debug_mode and (sort_column or query_text):
        base_table = active_table
        if base_table is None:
            log("No active result table to sort", file=sys.stderr)
            return 1

        working_table = _clone_table(base_table)
        rc = _apply_table_sort(working_table, sort_column=sort_column, query_text=query_text)
        if rc != 0:
            return rc

        items = list(ctx.get_last_result_items() or [])
        reordered_items = _reorder_items_from_table(working_table, items)
        subject = ctx.get_last_result_subject() if hasattr(ctx, "get_last_result_subject") else None
        ctx.set_last_result_table_overlay(working_table, reordered_items, subject)
        ctx.set_current_stage_table(working_table)
        return _render_table(working_table)

    state = None
    try:
        state = ctx.get_pipeline_state() if hasattr(ctx, "get_pipeline_state") else None
    except Exception:
        state = None

    def _summarize_table(name: str, t: Any) -> None:
        if t is None:
            print(f"{name}: None")
            return
        try:
            table_type = getattr(t, "table", None)
        except Exception:
            table_type = None
        try:
            title = getattr(t, "title", None)
        except Exception:
            title = None
        try:
            src_cmd = getattr(t, "source_command", None)
        except Exception:
            src_cmd = None
        try:
            src_args = getattr(t, "source_args", None)
        except Exception:
            src_args = None
        try:
            no_choice = bool(getattr(t, "no_choice", False))
        except Exception:
            no_choice = False
        try:
            preserve_order = bool(getattr(t, "preserve_order", False))
        except Exception:
            preserve_order = False
        try:
            row_count = len(getattr(t, "rows", []) or [])
        except Exception:
            row_count = 0
        try:
            meta = (
                t.get_table_metadata() if hasattr(t, "get_table_metadata") else getattr(t, "table_metadata", None)
            )
        except Exception:
            meta = None
        meta_keys = list(meta.keys()) if isinstance(meta, dict) else []

        print(
            f"{name}: id={id(t)} class={type(t).__name__} title={repr(title)} table={repr(table_type)} rows={row_count} "
            f"source={repr(src_cmd)} source_args={repr(src_args)} no_choice={no_choice} preserve_order={preserve_order} meta_keys={meta_keys}"
        )

    label = ""
    try:
        label = str(args[0]) if args else ""
        if label:
            print(f"Table State: {label}")
        else:
            print("Table State")
    except Exception:
        print("Table State")

    try:
        _summarize_table("display_table", getattr(state, "display_table", None) if state is not None else None)
        _summarize_table("current_stage_table", getattr(state, "current_stage_table", None) if state is not None else None)
        _summarize_table("last_result_table", getattr(state, "last_result_table", None) if state is not None else None)

        display_items = getattr(state, "display_items", None) if state is not None else None
        last_result_items = getattr(state, "last_result_items", None) if state is not None else None
        hist = getattr(state, "result_table_history", None) if state is not None else None
        fwd = getattr(state, "result_table_forward", None) if state is not None else None
        last_sel = getattr(state, "last_selection", None) if state is not None else None

        print(
            "buffers: "
            f"display_items={len(display_items or [])} "
            f"last_result_items={len(last_result_items or [])} "
            f"history={len(hist or [])} "
            f"forward={len(fwd or [])} "
            f"last_selection={list(last_sel or [])}"
        )
    except Exception as exc:
        log(f"Failed to summarize table state: {exc}")
        return 1

    # If debug logging is enabled, also emit the richer debug dump.
    try:
        if hasattr(ctx, "debug_table_state"):
            ctx.debug_table_state(label or ".table")
    except Exception:
        pass

    return 0


CMDLET = Cmdlet(
    name=".table",
    alias=["table"],
    summary="Render, inspect, or sort the active result table.",
    usage='.table [-sort <column>] [-query "format:asc|desc,namespace:track"] [-print -path <path>] [-debug [label]]',
    arg=[
        CmdletArg(
            name="sort",
            type="string",
            description="Sort by a visible column name (for namespace tag sorting, use -sort tag with -query namespace:<name>).",
            required=False,
        ),
        CmdletArg(
            name="query",
            type="string",
            description="Table options like format:asc|desc, namespace:track, numeric:true.",
            required=False,
        ),
        CmdletArg(
            name="print",
            type="flag",
            description="Export the active table as an SVG using -path.",
            required=False,
        ),
        SharedArgs.PATH,
        CmdletArg(
            name="debug",
            type="flag",
            description="Dump pipeline table state for debugging instead of rendering the table.",
            required=False,
        ),
        CmdletArg(
            name="label",
            type="string",
            description="Optional label to include in the debug dump",
            required=False,
        ),
    ],
)

CMDLET.exec = _run
