from __future__ import annotations

import re
import tempfile
import webbrowser
from pathlib import Path
from typing import TYPE_CHECKING, Literal

import narwhals.stable.v1 as nw
from great_tables import GT
from great_tables._export import _create_temp_file_server
from great_tables._gt_data import Boxhead, ColInfo
from great_tables._tbl_data import SelectExpr, is_na

from gt_extras._utils_column import _validate_and_get_single_column

__all__ = ["fmt_pct_extra", "gt_duplicate_column", "gt_two_column_layout"]


def fmt_pct_extra(
    gt: GT,
    columns: SelectExpr,
    scale: float = 100,
    threshold: float = 1.0,
    color: str = "grey",
    decimals: int = 1,
) -> GT:
    # TODO: would we want to infer the range? (as we do in gt_plt_bar_pct)
    """
    Convert to percent and show less than `1%` as `<1%` in grey.

    The `fmt_pct_extra()` function takes an existing `GT` object and formats a column of numeric
    values as percentages. Values below the specified threshold are displayed as `"<threshold%"`
    instead of their actual percentage value, and in a color of your choosing. The default behavior
    expects values in the range `[0, 1]`, use `scale` if your values are in a different range.

    Parameters
    ----------
    gt
        A `GT` object to modify.

    columns
        The columns containing numeric values to format as percentages.

    scale
        Multiplication factor to convert values to percentages. For some common cases:
        use `100` if values are decimals `(0.05 -> 5%)` (default),
        use `1` if values are already percentages `(5 -> 5%)`.

    threshold
        The percentage threshold below which values are displayed as `"<threshold%"` instead of
        their actual value. Note this refers to the scaled value, not the original, so you will
        have to provide `5` for `<5%` even if your original are in the range `[0,1]`.

    color
        The color to use for values below the threshold.

    decimals
        Number of decimal places to display for percentages.

    Returns
    -------
    GT
        A `GT` object with formatted percentage column.

    Examples
    --------
    ```{python}
    from great_tables import GT
    from great_tables.data import towny
    import gt_extras as gte

    towny_mini = towny[
        [
            "name",
            "pop_change_1996_2001_pct",
            "pop_change_2001_2006_pct",
            "pop_change_2006_2011_pct",
        ]
    ].tail(10)

    gt = (
        GT(towny_mini)
        .tab_spanner(label="Population Change", columns=[1, 2, 3])
        .cols_label(
            pop_change_1996_2001_pct="'96-'01",
            pop_change_2001_2006_pct="'01-'06",
            pop_change_2006_2011_pct="'06-'11",
        )
    )

    gt.pipe(
        gte.fmt_pct_extra,
        columns=[1, 2, 3],
        threshold=5,
        color="green",
    )
    ```
    """
    # TODO: consider how to handle negative values

    def _fmt_pct_single_val(value: float):
        if is_na(gt._tbl_data, value):
            return ""

        # Convert to percentage
        pct_value = value * scale

        if abs(pct_value) < threshold:
            return f"<span style='color:{color};'><{threshold:g}%</span>"
        else:
            return f"{pct_value:.{decimals}f}%"

    res = gt
    res = res.fmt(_fmt_pct_single_val, columns=columns)

    return res


def gt_duplicate_column(
    gt: GT,
    column: SelectExpr,
    after: str | None = None,
    append_text: str | None = "_dupe",
    dupe_name: str | None = None,
) -> GT:
    """
    Duplicate a column in a `GT` object.

    The `gt_duplicate_column()` function takes an existing `GT` object and creates a duplicate
    _(without styling)_ of the specified column. The duplicated column can be renamed using either
    `dupe_name` or by appending text to the original column name, and positioned at a
    specific location in the table.

    Parameters
    ----------
    gt
        A `GT` object to modify.

    column
        The column to duplicate. Can be a column name or index.

    after
        The column after which to place the duplicated column. If `None`, the duplicated
        column will be moved to the end of the table.

    append_text
        Text to append to the original column name for the duplicate. Only used if
        `dupe_name` is not provided. Defaults to `"_dupe"`.

    dupe_name
        The name for the duplicated column. If provided, this overrides `append_text`.

    Returns
    -------
    GT
        A `GT` object with the duplicated column added.

    Examples
    --------
    ```{python}
    from great_tables import GT
    from great_tables.data import gtcars
    import gt_extras as gte

    gtcars_mini = gtcars[["mfr", "model", "year", "hp"]].head(5)
    gt = GT(gtcars_mini)

    # Duplicate with custom name and position
    gt.pipe(
        gte.gt_duplicate_column,
        column="hp",
        after="year",
    )
    ```

    Note
    --------
    Styles generated by [`GT.tab_style()`](https://posit-dev.github.io/great-tables/reference/GT.tab_style)
    will render in the duplicated column. This may be accommodated in the future.
    """
    original_name, _ = _validate_and_get_single_column(gt, column)

    # If dupe_name is given, it overrides append_text
    append_text = append_text or "_dupe"
    if dupe_name is not None:
        new_col_name = dupe_name
    else:
        new_col_name = original_name + append_text

    if new_col_name == original_name:
        raise ValueError(
            f"The new column name '{new_col_name}' cannot be the same as the original column name '{original_name}'."
        )

    res = gt

    # get the boxhead info
    original_col_info = None
    for col_info in res._boxhead:
        if col_info.var == original_name:
            original_col_info = col_info
            break

    if TYPE_CHECKING:
        if original_col_info is None:
            raise ValueError(f"Column '{original_name}' not found in boxhead.")

    # make the new boxhead entry
    new_col_info = ColInfo(
        var=new_col_name,
        type=original_col_info.type,
        column_label=new_col_name,
        column_align=original_col_info.column_align,
        column_width=original_col_info.column_width,
    )

    # Duplicate the col in the data_table data
    original_data_table = res._tbl_data
    new_data_table = nw.to_native(
        nw.from_native(original_data_table).with_columns(
            nw.col(original_name).alias(new_col_name)
        )
    )

    # Duplicate the col in the body element of body
    new_body = res._body
    new_body.body = nw.to_native(
        nw.from_native(new_body.body).with_columns(
            nw.col(original_name).alias(new_col_name)
        )
    )

    new_boxhead_list = list(res._boxhead._d) + [new_col_info]
    new_boxhead = Boxhead(new_boxhead_list)

    res = res._replace(_tbl_data=new_data_table, _boxhead=new_boxhead, _body=new_body)

    if after is None:
        res = res.cols_move_to_end(new_col_name)
    else:
        res = res.cols_move(new_col_name, after=after)

    return res


class GTCombinedLayout:
    """
    Represents the combined layout of two `GT` objects rendered side by side.

    This class is returned by [`gt_two_column_layout()`](https://posit-dev.github.io/gt-extras/reference/gt_two_column_layout)
    and is designed for automatic rendering in Jupyter notebooks and other HTML-aware environments.

    Parameters
    ----------
    html_content
        The combined HTML content representing the two tables in a single layout.

    Methods
    -------
    _repr_html_()
        Returns the HTML content for notebook rendering.

    __str__()
        Returns the HTML content as a string.

    show()
        Displays the table in an environment of choice. Options are `"notebook"` and `"browser"`.
    """

    def __init__(self, html_content: str):
        self.html_content = html_content

    def _repr_html_(self):
        return self.html_content

    def __str__(self):
        return self.html_content

    # do a show based on code from great_tables `GT.show()`
    def show(self, target: Literal["notebook", "browser"] = "browser"):
        if target == "notebook":
            from IPython.core.display import display_html

            # https://github.com/ipython/ipython/pull/10962
            display_html(  # pyright: ignore[reportUnknownVariableType]
                self._repr_html_(), raw=True, metadata={"text/html": {"isolated": True}}
            )
        elif target == "browser":
            with tempfile.TemporaryDirectory() as tmp_dir:
                f_path = Path(tmp_dir) / "index.html"
                f_path.write_text(self._repr_html_(), encoding="utf-8")

                # create a server that closes after 1 request ----
                server = _create_temp_file_server(f_path)
                webbrowser.open(f"http://127.0.0.1:{server.server_port}/{f_path.name}")
                server.handle_request()
        else:
            raise Exception(f"Unknown target display: {target}")

    def save(self):
        raise NotImplementedError


def gt_two_column_layout(
    gt1: GT,
    gt2: GT,
    target: Literal["notebook", "browser"] | None = None,  # TODO: add save
    table_header_from: Literal[1, 2] | None = None,
) -> GTCombinedLayout:
    """
    Combine two `GT` objects into a two-column layout.

    This function takes two `GT` objects and arranges them side by side in a single HTML output.
    Optionally, the header (title and subtitle) from one of the tables can be included at the top
    of the combined layout. The output can be displayed directly in a Jupyter notebook, opened in
    a browser, or returned as HTML.

    Parameters
    ----------
    gt1
        The first table to include in the layout. This table will appear on the left side.

    gt2
        The second table to include in the layout. This table will appear on the right side.

    target
        Determines how the combined layout is displayed. Use `"notebook"` to display in a Jupyter
        notebook, `"browser"` to open in a web browser. If `None`, returns the layout object
        for automatic rendering. Currently, `"save"` is not yet implemented.

    table_header_from
        Determines which table's header (title and subtitle) to include in the combined layout.
        If set to `1`, the header from `gt1` will be used. If set to `2`, the header from `gt2`
        will be used. If `None`, no header will be included.

    Returns
    -------
    GTCombinedLayout
        A layout object that automatically renders as HTML in Jupyter notebooks and Quarto
        documents. It also has a `.show()` method, taking `"notebook"` or `"browser"` as targets.

    Examples
    --------
    ```{python}
    from great_tables import GT
    import gt_extras as gte
    import pandas as pd

    df1 = pd.DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]})
    df2 = pd.DataFrame({"X": [7, 8, 9], "Y": [10, 11, 12]})

    gt1 = GT(df1).tab_header(title="Table 1", subtitle="1st Table")
    gt2 = GT(df2).tab_header(title="Table 2", subtitle="2nd Table")

    gte.gt_two_column_layout(gt1, gt2)
    ```

    Alternatively, we can combine data from the same table in a two column layout like so:

    ```{python}
    df1 = pd.DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]})
    df2 = pd.DataFrame({"A": [7, 8, 9], "B": [10, 11, 12]})

    gt1 = GT(df1).tab_header(title="Joined Table")
    gt2 = GT(df2)

    gte.gt_two_column_layout(gt1, gt2, table_header_from=1)
    ```


    """

    def extract_tab_header_and_style(gt: GT) -> dict:
        """
        Extract the title, subtitle, and full style block from a GT object's HTML.
        """
        html = gt.as_raw_html()

        title = gt._heading.title if gt._heading.title else ""
        subtitle = gt._heading.subtitle if gt._heading.subtitle else ""

        # Extract the original table ID
        id_match = re.search(r'<div id="([^"]*)"', html)
        original_id = id_match.group(1) if id_match else ""

        # Extract the entire style block
        style_match = re.search(r"<style>(.*?)</style>", html, re.DOTALL)
        style_content = style_match.group(1) if style_match else ""

        # Replace the original ID with "mycombinedtable" in the CSS
        if original_id:
            style_content = style_content.replace(f"#{original_id}", "#mycombinedtable")
            style_content = style_content.replace(
                "mycombinedtable table", "mycombinedtable div"
            )

        title_td_pattern = (
            r'<td[^>]*class="[^"]*gt_title[^"]*"[^>]*>\s*'
            + re.escape(str(title))
            + r"\s*</td>"
        )
        title_match = re.search(title_td_pattern, html, re.DOTALL)

        if title_match:
            td_tag = title_match.group(0)

            # Extract class attribute
            class_match = re.search(r'class="([^"]*gt_title[^"]*)"', td_tag)
            title_class = f"gt_table {class_match.group(1)}" if class_match else ""

            # Extract style attribute
            style_match = re.search(r'style="([^"]*)"', td_tag)
            title_inline_style = style_match.group(1) if style_match else ""
        else:
            title_class = ""
            title_inline_style = ""

        subtitle_class = ""

        subtitle_td_pattern = (
            r'<td[^>]*class="[^"]*gt_subtitle[^"]*"[^>]*>\s*'
            + re.escape(str(subtitle))
            + r"\s*</td>"
        )
        subtitle_match = re.search(subtitle_td_pattern, html, re.DOTALL)

        if subtitle_match:
            td_tag = subtitle_match.group(0)

            # Extract class attribute
            class_match = re.search(r'class="([^"]*gt_subtitle[^"]*)"', td_tag)

            if class_match:
                # Exclude gt_bottom_border from class string if present
                class_str = class_match.group(1)
                class_str = " ".join(
                    c for c in class_str.split() if c != "gt_bottom_border"
                )
                subtitle_class = f"gt_table {class_str}"

            # Extract style attribute
            style_match = re.search(r'style="([^"]*)"', td_tag)
            subtitle_inline_style = style_match.group(1) if style_match else ""
        else:
            subtitle_inline_style = ""

        return {
            "title": title,
            "subtitle": subtitle,
            "title_class": title_class,
            "subtitle_class": subtitle_class,
            "title_style": title_inline_style,
            "subtitle_style": subtitle_inline_style,
            "style": style_content,
        }

    # Extract header data if specified
    if table_header_from == 1:
        header_data = extract_tab_header_and_style(gt1)
    elif table_header_from == 2:
        header_data = extract_tab_header_and_style(gt2)
    else:
        header_data = None

    # Build the header HTML if header data is available
    if header_data and (header_data["title"] or header_data["subtitle"]):
        header_html = f"""
        <style>
        {header_data["style"]}
        </style>
        """

        # Add title if it exists
        if header_data["title"]:
            header_html += f"""
            <div class="{header_data["title_class"]}"
                style="width:100%; {header_data["title_style"]}"
            >
                {header_data["title"]}
            </div>
            """

        # Add subtitle if it exists
        if header_data["subtitle"]:
            header_html += f"""
            <div class="{header_data["subtitle_class"]}"
                style="width:100%; {header_data["subtitle_style"]}">
                {header_data["subtitle"]}
            </div>
            """

        # Remove headers from the two tables
        gt1_no_header = gt1.tab_header(title=None, subtitle=None)  # type: ignore
        table_1_html = gt1_no_header.as_raw_html()
        gt2_no_header = gt2.tab_header(title=None, subtitle=None)  # type: ignore
        table_2_html = gt2_no_header.as_raw_html()

    else:
        header_html = ""
        table_1_html = gt1.as_raw_html()
        table_2_html = gt2.as_raw_html()

    double_table_html = f"""
    <div style="display: flex; justify-content: center; width: 100%;">
        <div id="mycombinedtable" style="display: inline-block; width: auto;">
            {header_html}
            <div style="overflow: auto; white-space: nowrap;">
                <div style="display: inline-block; margin-right: 1em;">
                    {table_1_html}
                </div>
                <div style="display: inline-block;">
                    {table_2_html}
                </div>
            </div>
        </div>
    </div>
    """

    double_table = GTCombinedLayout(double_table_html)

    if target:
        if target == "save":
            raise NotImplementedError(
                "At the moment, only notebook and browser display options are available."
            )
        else:
            double_table.show(target)

    return GTCombinedLayout(double_table_html)
