"""
Absfuyu: Data Analysis
----------------------
DF Function

Version: 6.8.0
Date updated: 24/05/2026 (dd/mm/yyyy)
"""

# Module level
# ---------------------------------------------------------------------------
__all__ = [
    "equalize_df",
    "compare_2_list",
    "rename_with_dict",
    "merge_data_files",
    "dadf_output_wrapper",
    "read_excel_expand_merged",
    "read_excel_expand_merged_dadf",
    # Type
    "DataFileExt",
]


# Library
# ---------------------------------------------------------------------------
from collections.abc import Callable, Mapping, Sequence
from functools import wraps
from itertools import chain
from pathlib import Path
from typing import Literal, ParamSpec, cast, overload

import numpy as np
import pandas as pd
from openpyxl import load_workbook
from openpyxl.worksheet.merge import MergedCellRange

from absfuyu.core.decorator import deprecated as wdeprecated
from absfuyu.core.docstring import deprecated, versionadded, versionchanged
from absfuyu.core.dummy_func import tqdm
from absfuyu.extra.da.dadf import DADF

# Typing
# ---------------------------------------------------------------------------
P = ParamSpec("P")
type DataFileExt = Literal[".csv", ".xls", ".xlsx", ".xlsm", ".xlsb", ".ods"]


# Function
# ---------------------------------------------------------------------------
@wdeprecated(version="6.2.0")
@deprecated("6.2.0")
def equalize_df(data: dict[str, list], fillna=np.nan) -> dict[str, list]:
    """
    Make all list in dict have equal length to make pd.DataFrame

    :param data: `dict` data that ready for `pd.DataFrame`
    :param fillna: Fill N/A value (Default: `np.nan`)
    """
    max_len = max(map(len, data.values()))
    for _, v in data.items():
        if len(v) < max_len:
            missings = max_len - len(v)
            for _ in range(missings):
                v.append(fillna)
    return data


@wdeprecated(version="6.2.0")
@deprecated("6.2.0")
def compare_2_list(*arr) -> pd.DataFrame:
    """
    Compare 2 lists then create DataFrame
    to see which items are missing

    Parameters
    ----------
    arr : list
        List

    Returns
    -------
    DataFrame
        Compare result
    """
    # Setup
    col_name = "list"
    arr = [sorted(x) for x in arr]  # type: ignore # map(sorted, arr)

    # Total array
    tarr = sorted(list(set(chain.from_iterable(arr))))
    # max_len = len(tarr)

    # Temp dataset
    temp_dict = {"base": tarr}
    for idx, x in enumerate(arr):
        name = f"{col_name}{idx}"

        # convert list
        temp = [item if item in x else np.nan for item in tarr]

        temp_dict.setdefault(name, temp)

    df = pd.DataFrame(temp_dict)
    df["Compare"] = np.where(
        df[f"{col_name}0"].apply(lambda x: str(x).lower()) == df[f"{col_name}1"].apply(lambda x: str(x).lower()),
        df[f"{col_name}0"],  # Value when True
        np.nan,  # Value when False
    )
    return df


@wdeprecated(version="6.2.0")
@deprecated("6.2.0")
def rename_with_dict(df: pd.DataFrame, col: str, rename_dict: dict) -> pd.DataFrame:
    """
    Version: 2.0.0

    :param df: DataFrame
    :param col: Column name
    :param rename_dict: Rename dictionary
    """

    name = f"{col}_filtered"
    df[name] = df[col]
    rename_val = list(rename_dict.keys())
    df[name] = df[name].apply(lambda x: "Other" if x in rename_val else x)
    return df


# MARK: Merge
@overload
def merge_data_files(
    work_dir: Path | str,
    file_type: DataFileExt | Sequence[DataFileExt] = ".xlsx",
    output_file: Path | str | None = None,
    *,
    recursive: bool = True,
    export_file: bool = True,
    drop_duplicates: bool = True,
    no_return: Literal[True] = True,
    tqdm_enabled: bool = True,
) -> None: ...


@overload
def merge_data_files(
    work_dir: Path | str,
    file_type: DataFileExt | Sequence[DataFileExt] = ".xlsx",
    output_file: Path | str | None = None,
    *,
    recursive: bool = True,
    export_file: bool = True,
    drop_duplicates: bool = True,
    no_return: Literal[False] = ...,
    tqdm_enabled: bool = True,
) -> pd.DataFrame: ...


@versionchanged("6.9.0", reason="New parameters")
@versionadded("6.0.0")
def merge_data_files(
    work_dir: Path | str,
    file_type: DataFileExt | Sequence[DataFileExt] = ".xlsx",
    output_file: Path | str | None = None,
    *,
    recursive: bool = True,
    export_file: bool = True,
    drop_duplicates: bool = True,
    no_return: bool = True,
    tqdm_enabled: bool = True,
) -> pd.DataFrame | None:
    """
    Merge all data-sheet-like (.csv, .xls, .xlsx) in a folder/directory.
    Also remove duplicate rows (togglable)

    Parameters
    ----------
    work_dir : Path | str
        Files in which folder/directory

    file_type : DataFileExt | Sequence[DataFileExt], optional
        File format, by default ``".xlsx"``

    output_file : Path | str | None, optional
        | Output file location, by default ``None``
        | File will be export in ``.xlsx`` format
        | Default export name is ``data_merged.xlsx``

    recursive : bool, optional
        Also merge file in subfolder, by default ``True``

    export_file : bool, optional
        Export to file, by default ``True``

    drop_duplicates : bool, optional
        Drop duplicates from data, by default ``True``

    no_return : bool, optional
        Return DataFrame when ``False``, by default ``True``

    tqdm_enabled : bool, optional
        Use ``tqdm`` package to show progress bar (if available), by default ``True``

    Returns
    -------
    DataFrame
        Merged DataFrame

    None
        When no_return=True
    """

    # Resolve path
    default_name = "data_merged.xlsx"
    if isinstance(file_type, str):
        file_type = [file_type]

    pattern = "**/*" if recursive else "*"
    paths = [x for x in Path(work_dir).glob(pattern) if x.name != default_name and x.suffix in set(file_type)]
    print(paths)
    output_path = Path(output_file) if output_file is not None else Path(work_dir).joinpath(default_name)

    # Quick exit
    if no_return and not export_file:
        return None

    # Process
    readers = {
        ".csv": pd.read_csv,
        ".xls": pd.read_excel,
        ".xlsx": pd.read_excel,
    }
    dfs = []
    iterator = tqdm(paths, desc="Merging files", unit_scale=True) if tqdm_enabled else paths
    for x in iterator:
        dfs.append(readers.get(x.suffix.lower(), pd.read_excel)(x))
        # try:
        #     dfs.append(readers[x.suffix.lower()](x))
        # except KeyError:
        #     raise ValueError(f"Unsupported file type: {x.suffix}")

    df = cast(pd.DataFrame, pd.concat(dfs, axis=0, join="inner"))
    if drop_duplicates:
        df = df.drop_duplicates().reset_index()
        df.drop(columns=["index"], inplace=True)

    # Export
    if export_file:
        df.to_excel(output_path, index=False)

    # Return
    if no_return:
        return None
    return df


# MARK: Export
@deprecated("6.8.0", "Use ``absfuyu.extra.da.export`` module")
@versionadded("6.0.0")
def export_dfs_to_excel(
    path: str,
    dfs: Mapping[str, pd.DataFrame],
    *,
    index: bool = False,
) -> None:
    """
    Export multiple DataFrames into one Excel file.

    Parameters
    ----------
    path : str
        Output Excel file path.

    dfs : Mapping[str, DataFrame]
        Sheet name -> DataFrame mapping.

    index : bool, default False
        Whether to include DataFrame index, by default ``False``
    """
    with pd.ExcelWriter(path, engine="openpyxl") as writer:
        for name, df in dfs.items():
            name = name[:31]  # Excel sheet name length limit
            df.to_excel(writer, sheet_name=name, index=index)


# MARK: Output wrapper
def dadf_output_wrapper(obj: Callable[P, pd.DataFrame]) -> Callable[P, DADF]:
    """
    This return ``DADF`` instead of ``DataFrame`` in a Callable
    """

    if isinstance(obj, type):
        return obj

    @wraps(obj)
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> DADF:
        result = obj(*args, **kwargs)
        return DADF(result)

    return wrapper


# MARK: Expand merged cells
@versionadded("6.8.0")
def read_excel_expand_merged(
    file_path: str | Path,
    sheet_name: str | int | None = 0,
    header: int | list[int] | None = 0,
) -> pd.DataFrame:
    """
    Read an Excel sheet and expand merged-cell values into
    the entire merged region.

    Parameters
    ----------
    file_path : str | Path
        Path to the Excel file.

    sheet_name : str | int | None
        Sheet name or sheet index, by default ``0``

    header : int | list[int] | None
        Row(s) to use as the column header, identical to
        ``pandas.read_excel(header=...)``, by default ``0``

        Examples
        --------
        - ``0``     -> first row is header
        - ``1``     -> second row is header
        - ``[0,1]`` -> multi-level header
        - ``None``  -> no header

    Returns
    -------
    pd.DataFrame
        DataFrame with merged-cell values expanded.


    Notes
    -----
    - Uses ``openpyxl`` to detect merged ranges.
    - Automatically adjusts row offsets based on header rows.
    - Supports multi-index headers.
    """

    filepath = Path(file_path)

    # Load workbook
    wb = load_workbook(filepath, data_only=True)

    if isinstance(sheet_name, int):
        ws = wb.worksheets[sheet_name]
    else:
        ws = wb[sheet_name]

    # Read dataframe
    df = pd.read_excel(
        filepath,
        sheet_name=sheet_name,
        header=header,
    )

    merged_ranges: set[MergedCellRange] = ws.merged_cells.ranges

    # Calculate dataframe row offset
    #
    # Excel row -> DataFrame row conversion:
    # - openpyxl rows are 1-based
    # - pandas data rows start AFTER header rows
    #
    # Examples:
    # header=0     -> offset = 2
    # header=2     -> offset = 4
    # header=[0,1] -> offset = 3
    # header=None  -> offset = 1

    if header is None:
        row_offset = 1

    elif isinstance(header, int):
        row_offset = header + 2

    else:
        row_offset = max(header) + 2

    for merged_range in merged_ranges:
        min_row = merged_range.min_row
        max_row = merged_range.max_row
        min_col = merged_range.min_col
        max_col = merged_range.max_col

        # Convert Excel coordinates -> DataFrame coordinates
        df_row_start = min_row - row_offset
        df_row_end = max_row - row_offset

        df_col_start = min_col - 1
        df_col_end = max_col - 1

        # Skip header-area merges
        if df_row_end < 0:
            continue

        # Clamp lower bounds
        df_row_start = max(df_row_start, 0)
        df_col_start = max(df_col_start, 0)

        # Skip invalid/out-of-range regions
        if df_row_start >= len(df) or df_col_start >= len(df.columns):
            continue

        region = df.iloc[
            df_row_start : df_row_end + 1,
            df_col_start : df_col_end + 1,
        ]

        # # Expand merged values
        # df.iloc[
        #     df_row_start : df_row_end + 1,
        #     df_col_start : df_col_end + 1,
        # ] = (
        #     region.ffill(axis=0).ffill(axis=1).infer_objects(copy=False)
        # )

        with pd.option_context("future.no_silent_downcasting", True):
            # Expand merged values
            expanded = region.ffill(axis=0).ffill(axis=1).infer_objects(copy=False)

        df.iloc[
            df_row_start : df_row_end + 1,
            df_col_start : df_col_end + 1,
        ] = expanded

    return df


read_excel_expand_merged_dadf = dadf_output_wrapper(read_excel_expand_merged)
