"""pubtab — Excel to publication-ready LaTeX tables."""

from __future__ import annotations

from importlib.metadata import version as _version

__version__ = _version("pubtab")

from pathlib import Path
from typing import Callable, Dict, List, Optional, Union

from .models import Cell, SpacingConfig, TableData
from .reader import list_excel_sheets, read_excel
from .renderer import render

__all__ = ["xlsx2tex", "preview", "compile_pdf", "tex_to_excel", "SpacingConfig"]

# Sentinel for detecting unset kwargs
_UNSET = object()


def _build_sheet_output_paths(
    input_file: Union[str, Path],
    output: Union[str, Path],
    sheet_count: int,
) -> List[Path]:
    """Build output paths for one or multiple sheet exports."""
    output_path = Path(output)
    if sheet_count <= 1:
        if output_path.suffix.lower() == ".tex":
            return [output_path]
        return [output_path / f"{Path(input_file).stem}.tex"]

    if output_path.suffix.lower() == ".tex":
        base_dir = output_path.parent
        base_stem = output_path.stem
    else:
        base_dir = output_path
        base_stem = Path(input_file).stem
    return [base_dir / f"{base_stem}_sheet{i + 1:02d}.tex" for i in range(sheet_count)]


def _iter_input_files(directory: Path, suffixes: tuple[str, ...]) -> List[Path]:
    """List input files in a directory by suffix, sorted by name."""
    return sorted(
        [
            p for p in directory.iterdir()
            if p.is_file() and p.suffix.lower() in suffixes
        ],
        key=lambda p: p.name.lower(),
    )


def xlsx2tex(
    input_file: Union[str, Path],
    output: Union[str, Path],
    config: Optional[Union[str, Path]] = None,
    sheet: Optional[Union[str, int]] = _UNSET,
    theme: str = _UNSET,
    caption: Optional[str] = _UNSET,
    label: Optional[str] = _UNSET,
    header_rows: int = _UNSET,
    position: Optional[str] = _UNSET,
    spacing: Optional[SpacingConfig] = _UNSET,
    font_size: Optional[str] = _UNSET,
    resizebox: Optional[str] = _UNSET,
    col_spec: Optional[str] = _UNSET,
    header_sep: Optional[str] = _UNSET,
    header_cmidrule: bool = _UNSET,
    span_columns: bool = _UNSET,
    custom_header: Optional[List[List[Cell]]] = _UNSET,
    group_separators: Optional[Union[List[int], Dict[int, str]]] = _UNSET,
    cell_formatter: Optional[Callable[[int, int, Cell], Cell]] = _UNSET,
    num_cols: Optional[int] = _UNSET,
    preview: bool = _UNSET,
    preamble: Optional[str] = _UNSET,
    dpi: int = _UNSET,
    upright_scripts: bool = _UNSET,
    # Deprecated aliases
    wide: bool = _UNSET,
    raw_caption: bool = _UNSET,
) -> str:
    """Convert Excel file(s) to LaTeX and write .tex file(s).

    Args:
        input_file: Path to .xlsx/.xls file, or a directory containing them.
        output: Output .tex path or output directory.
            - Single file input: use a `.tex` path (or a directory path).
            - Directory input: must be a directory path.
            - If multiple sheets are exported, files are saved as
              `stem_sheetNN.tex`.
        config: Path to YAML config file (explicit kwargs override config values).
        sheet: Sheet name or 0-based index.
        theme: Theme name.
        caption: Table caption (always passed as-is, no escaping).
        label: LaTeX label.
        header_rows: Number of header rows in Excel.
        position: Table float position (e.g. "t", "htbp").
        spacing: Override spacing config.
        font_size: Override font size (e.g. "footnotesize").
        resizebox: Resize width (e.g. "0.8\\textwidth").
        col_spec: Column specification (e.g. "lccc").
        header_sep: Custom header separator (auto-generated from merged cells if None).
        span_columns: Use table* for two-column spanning.
        custom_header: Custom header rows (replaces Excel headers).
        group_separators: List[int] row indices or Dict[int, str] for custom separators.
        cell_formatter: Callback(row_idx, col_idx, cell) -> Cell.
        num_cols: Override column count.
        preview: Generate PNG preview.
        preamble: Extra LaTeX preamble for preview.
        dpi: Preview resolution.
        wide: Deprecated alias for span_columns.
        raw_caption: Deprecated, ignored.

    Returns:
        LaTeX table string of the first exported sheet.
    """
    input_path = Path(input_file)
    if input_path.is_dir():
        output_path = Path(output)
        if output_path.suffix.lower() == ".tex":
            raise ValueError(
                "When input_file is a directory, output must be a directory path."
            )
        output_path.mkdir(parents=True, exist_ok=True)
        excel_files = _iter_input_files(input_path, (".xlsx", ".xls"))
        if not excel_files:
            raise ValueError(f"No .xlsx/.xls files found in directory: {input_path}")

        first_tex = ""
        for excel_file in excel_files:
            tex_out = output_path / f"{excel_file.stem}.tex"
            tex = xlsx2tex(
                excel_file,
                tex_out,
                config=config,
                sheet=sheet,
                theme=theme,
                caption=caption,
                label=label,
                header_rows=header_rows,
                position=position,
                spacing=spacing,
                font_size=font_size,
                resizebox=resizebox,
                col_spec=col_spec,
                header_sep=header_sep,
                header_cmidrule=header_cmidrule,
                span_columns=span_columns,
                custom_header=custom_header,
                group_separators=group_separators,
                cell_formatter=cell_formatter,
                num_cols=num_cols,
                preview=preview,
                preamble=preamble,
                dpi=dpi,
                upright_scripts=upright_scripts,
                wide=wide,
                raw_caption=raw_caption,
            )
            if not first_tex:
                first_tex = tex
        return first_tex

    # Defaults
    defaults = dict(
        sheet=None, theme="three_line", caption=None, label=None,
        header_rows=None, position="htbp", spacing=None,
        font_size=None, resizebox=None, col_spec=None, header_sep=None,
        header_cmidrule=True, span_columns=False, custom_header=None, group_separators=None,
        cell_formatter=None, num_cols=None, preview=False, preamble=None,
        dpi=300, upright_scripts=False,
    )

    # Load YAML config
    cfg_formatter = None
    if config is not None:
        from .config import load_config
        cfg_kwargs, cfg_formatter = load_config(config)
        defaults.update(cfg_kwargs)

    # Explicit kwargs override config and defaults
    # Handle deprecated wide → span_columns alias
    _span = span_columns if span_columns is not _UNSET else (wide if wide is not _UNSET else _UNSET)
    explicit = dict(
        sheet=sheet, theme=theme, caption=caption, label=label,
        header_rows=header_rows, position=position,
        spacing=spacing, font_size=font_size, resizebox=resizebox,
        col_spec=col_spec, header_sep=header_sep, header_cmidrule=header_cmidrule,
        span_columns=_span,
        custom_header=custom_header,
        group_separators=group_separators,
        cell_formatter=cell_formatter, num_cols=num_cols, preview=preview,
        preamble=preamble, dpi=dpi, upright_scripts=upright_scripts,
    )
    p = {k: (v if v is not _UNSET else defaults[k]) for k, v in explicit.items()}

    if p["cell_formatter"] is None and cfg_formatter is not None:
        p["cell_formatter"] = cfg_formatter

    if p["sheet"] is None:
        sheet_names = list_excel_sheets(input_path)
        sheet_selectors: List[Union[str, int]] = list(sheet_names)
    else:
        sheet_selectors = [p["sheet"]]

    output_paths = _build_sheet_output_paths(input_path, output, len(sheet_selectors))
    tex_outputs: List[str] = []

    for idx, sheet_selector in enumerate(sheet_selectors):
        table = read_excel(input_path, sheet=sheet_selector, header_rows=p["header_rows"])

        if p["custom_header"] is not None:
            data_rows = table.cells[table.header_rows:]
            if p["cell_formatter"]:
                data_rows = [
                    [p["cell_formatter"](r, c, cell) for c, cell in enumerate(row)]
                    for r, row in enumerate(data_rows)
                ]
            all_cells = p["custom_header"] + data_rows
            hc = len(p["custom_header"])
            nc = p["num_cols"] or table.num_cols
            table = TableData(
                cells=all_cells, num_rows=len(all_cells), num_cols=nc,
                header_rows=hc, group_separators=p["group_separators"] or {},
            )
        elif p["group_separators"]:
            table = TableData(
                cells=table.cells, num_rows=table.num_rows, num_cols=table.num_cols,
                header_rows=table.header_rows, group_separators=p["group_separators"],
            )

        tex = render(
            table, theme=p["theme"], caption=p["caption"], label=p["label"],
            position=p["position"], spacing=p["spacing"],
            font_size=p["font_size"], resizebox=p["resizebox"], col_spec=p["col_spec"],
            header_sep=p["header_sep"], header_cmidrule=p["header_cmidrule"],
            span_columns=p["span_columns"], upright_scripts=p["upright_scripts"],
        )

        output_path = output_paths[idx]
        output_path.parent.mkdir(parents=True, exist_ok=True)
        output_path.write_text(tex)
        tex_outputs.append(tex)

        if p["preview"]:
            from ._preview import preview as _preview
            png_path = output_path.with_suffix(".png")
            _preview(output_path, output=png_path, theme=p["theme"], dpi=p["dpi"],
                     preamble=p["preamble"])

    return tex_outputs[0] if tex_outputs else ""


def preview(
    tex_input: Union[str, Path],
    output: Optional[Union[str, Path]] = None,
    theme: str = "three_line",
    dpi: int = 300,
    preamble: Optional[str] = None,
    format: str = "png",
) -> Path:
    """Generate preview(s) from LaTeX content, .tex file, or directory.

    Args:
        tex_input: LaTeX string, path to .tex file, or directory of `.tex` files.
        output: Output file path or output directory.
            For directory input, defaults to `<tex_input>/preview_<format>/`.
        theme: Theme name.
        dpi: Resolution (PNG only).
        preamble: Extra LaTeX preamble (e.g. custom commands).
        format: Output format, "png" or "pdf".

    Returns:
        Path to generated file (first output for directory input).
    """
    tex_path = Path(tex_input)
    if tex_path.exists() and tex_path.is_dir():
        tex_files = _iter_input_files(tex_path, (".tex",))
        if not tex_files:
            raise ValueError(f"No .tex files found in directory: {tex_path}")

        if output is None:
            output_dir = tex_path / f"preview_{format}"
        else:
            output_dir = Path(output)
            if output_dir.suffix.lower() in (".png", ".pdf"):
                raise ValueError(
                    "When tex_input is a directory, output must be a directory path."
                )
        output_dir.mkdir(parents=True, exist_ok=True)

        first_output: Optional[Path] = None
        for tex_file in tex_files:
            out_file = output_dir / f"{tex_file.stem}.{format}"
            if format == "pdf":
                generated = compile_pdf(tex_file, output=out_file, theme=theme, preamble=preamble)
            else:
                from ._preview import preview as _preview
                generated = _preview(tex_file, output=out_file, theme=theme, dpi=dpi, preamble=preamble)
            if first_output is None:
                first_output = generated
        if first_output is None:
            raise RuntimeError("No preview output was generated.")
        return first_output

    if format == "pdf":
        return compile_pdf(tex_input, output=output, theme=theme, preamble=preamble)

    from ._preview import preview as _preview
    return _preview(tex_input, output=output, theme=theme, dpi=dpi, preamble=preamble)


def compile_pdf(
    tex_input: Union[str, Path],
    output: Optional[Union[str, Path]] = None,
    theme: str = "three_line",
    preamble: Optional[str] = None,
) -> Path:
    """Compile LaTeX content or .tex file to PDF.

    Args:
        tex_input: LaTeX string or path to .tex file.
        output: Output PDF path. Defaults to input stem + .pdf.
        theme: Theme name (for package imports).
        preamble: Extra LaTeX preamble.

    Returns:
        Path to generated PDF.
    """
    from ._preview import compile_pdf as _compile_pdf

    tex_path = Path(tex_input)
    if tex_path.exists():
        tex_content = tex_path.read_text()
        if output is None:
            output = tex_path.with_suffix(".pdf")
    else:
        tex_content = str(tex_input)
        if output is None:
            output = Path("output.pdf")

    return _compile_pdf(tex_content, output, theme=theme, preamble=preamble)


def tex_to_excel(
    input_file: Union[str, Path],
    output: Union[str, Path],
) -> Path:
    """Convert LaTeX file(s) to Excel .xlsx.

    For multi-table files, each table is written to a separate sheet.
    For directory input, each `.tex` file is converted to one `.xlsx` file.

    Args:
        input_file: Path to .tex file, or a directory containing `.tex` files.
        output: Output .xlsx file path or output directory.
            For directory input, must be a directory path.

    Returns:
        Path to generated .xlsx file (first output for directory input).
    """
    from .tex_reader import read_tex_multi
    from .writer import write_excel, write_excel_multi

    input_path = Path(input_file)
    if input_path.is_dir():
        output_path = Path(output)
        if output_path.suffix.lower() == ".xlsx":
            raise ValueError(
                "When input_file is a directory, output must be a directory path."
            )
        output_path.mkdir(parents=True, exist_ok=True)
        tex_files = _iter_input_files(input_path, (".tex",))
        if not tex_files:
            raise ValueError(f"No .tex files found in directory: {input_path}")

        first_output: Optional[Path] = None
        for tex_file in tex_files:
            out_file = output_path / f"{tex_file.stem}.xlsx"
            generated = tex_to_excel(tex_file, out_file)
            if first_output is None:
                first_output = generated
        if first_output is None:
            raise RuntimeError("No xlsx output was generated.")
        return first_output

    output_path = Path(output)
    if output_path.suffix.lower() != ".xlsx":
        output_path.mkdir(parents=True, exist_ok=True)
        output_path = output_path / f"{input_path.stem}.xlsx"

    tex_content = input_path.read_text()
    tables = read_tex_multi(tex_content)
    if len(tables) == 1:
        return write_excel(tables[0], output_path)
    return write_excel_multi(tables, output_path)


# Deprecated aliases
convert = xlsx2tex
generate_preview = preview
