from __future__ import annotations

from xml.etree.ElementTree import Element

from openpyxl.comments import Comment
from openpyxl.utils import column_index_from_string, get_column_letter
from openpyxl.utils.cell import coordinate_from_string, range_boundaries
from openpyxl.worksheet.worksheet import Worksheet

from ._parser import (
    coerce_value,
    compute_span_bounds,
    parse_bool,
    parse_column,
    parse_column_range,
    parse_span,
)
from ._styling import (
    apply_styles_to_range,
    merge_styles,
    normalize_color,
    parse_style_string,
)


def build_sheet(ws: Worksheet, sheet_node: Element) -> None:
    default_style = parse_style_string(sheet_node.get("style"))

    if tab_color := sheet_node.get("tab-color"):
        color = normalize_color(tab_color)
        if color:
            ws.sheet_properties.tabColor = color

    if freeze := sheet_node.get("freeze"):
        ws.freeze_panes = freeze

    if gridlines := sheet_node.get("gridlines"):
        ws.sheet_view.showGridLines = parse_bool(value=gridlines, default=True)

    if default_width := sheet_node.get("default-column-width"):
        ws.sheet_format.defaultColWidth = float(default_width)

    if default_height := sheet_node.get("default-row-height"):
        ws.sheet_format.defaultRowHeight = float(default_height)

    _process_children(ws, sheet_node, default_style)


def _apply_column(ws: Worksheet, element: Element) -> None:
    width_attr = element.get("width")
    if not width_attr:
        return

    column_ref = element.get("column")
    range_attr = element.get("range")
    width = float(width_attr)

    if range_attr:
        start, end = parse_column_range(range_attr)
        for col_idx in range(start, end + 1):
            ws.column_dimensions[get_column_letter(col_idx)].width = width
    elif column_ref:
        ws.column_dimensions[get_column_letter(parse_column(column_ref))].width = width


def _apply_row(ws: Worksheet, element: Element, sheet_style: dict[str, str]) -> None:
    row_attr = element.get("index")
    if not row_attr:
        raise ValueError("<row> requires index attribute")

    row_index = int(row_attr)
    if height := element.get("height"):
        ws.row_dimensions[row_index].height = float(height)

    row_style = merge_styles(sheet_style, parse_style_string(element.get("style")))
    cursor = parse_column(element.get("start-column") or "1")

    _render_row_content(
        ws=ws,
        parent=element,
        inherited_style=row_style,
        default_row=row_index,
        cursor=cursor,
    )


def _render_cell(
    ws: Worksheet,
    element: Element,
    inherited_style: dict[str, str],
    default_row: int | None,
    default_column: int | None,
) -> tuple[int, int]:
    row, column = _resolve_coordinates(element, default_row, default_column)

    merge_range = element.get("merge")
    rowspan = parse_span(element.get("rowspan"))
    colspan = parse_span(element.get("colspan"))

    if merge_range:
        min_col, min_row, max_col, max_row = range_boundaries(merge_range)
        start_row, end_row = min_row, max_row
        start_column, end_column = min_col, max_col
        ws.merge_cells(merge_range)
    elif rowspan != 1 or colspan != 1:
        start_row, end_row = compute_span_bounds(row, rowspan)
        start_column, end_column = compute_span_bounds(column, colspan)
        ws.merge_cells(
            start_row=start_row,
            end_row=end_row,
            start_column=start_column,
            end_column=end_column,
        )
    else:
        start_row = row
        end_row = row
        start_column = column
        end_column = column

    anchor_row = int(start_row)
    anchor_column = int(start_column)
    cell = ws.cell(row=anchor_row, column=anchor_column)

    formula = element.get("formula")
    text_value = element.get("value")
    if text_value is None:
        text_value = "".join(element.itertext()).strip()
    value_type = element.get("type")

    if formula:
        cell.value = f"={formula.lstrip('=')}"
    else:
        cell.value = coerce_value(text_value, value_type)

    if hyperlink := element.get("hyperlink"):
        cell.hyperlink = hyperlink
    if comment := element.get("comment"):
        cell.comment = Comment(comment, element.get("comment-author") or "XML")

    combined_style = merge_styles(
        inherited_style, parse_style_string(element.get("style"))
    )
    apply_styles_to_range(
        ws.cell,
        combined_style,
        start_row=int(start_row),
        end_row=int(end_row),
        start_column=int(start_column),
        end_column=int(end_column),
    )

    return anchor_row, anchor_column


def _resolve_coordinates(
    element: Element,
    fallback_row: int | None,
    fallback_column: int | None,
) -> tuple[int, int]:
    if ref := element.get("ref"):
        column_letter, row = coordinate_from_string(ref.upper())
        column = column_index_from_string(column_letter)
        return int(row), int(column)

    row_attr = element.get("row")
    column_attr = element.get("column")

    if row_attr is None and fallback_row is None:
        raise ValueError("<cell> missing row reference")
    if column_attr is None and fallback_column is None:
        raise ValueError("<cell> missing column reference")

    row = int(row_attr) if row_attr is not None else fallback_row
    column = parse_column(column_attr) if column_attr is not None else fallback_column
    if row is None or column is None:
        raise ValueError("<cell> missing coordinates")
    return int(row), int(column)


def _process_children(
    ws: Worksheet,
    parent: Element,
    inherited_style: dict[str, str],
) -> None:
    for child in parent:
        tag = child.tag.lower()
        if tag == "style":
            block_style = merge_styles(
                inherited_style, parse_style_string(child.get("style"))
            )
            _process_children(ws, child, block_style)
        elif tag == "column":
            _apply_column(ws, child)
        elif tag == "row":
            _apply_row(ws, child, inherited_style)
        elif tag == "cell":
            _render_cell(ws, child, inherited_style, None, None)
        elif tag == "merge":
            cell_range = child.get("range")
            if cell_range:
                ws.merge_cells(cell_range)


def _render_row_content(
    ws: Worksheet,
    parent: Element,
    inherited_style: dict[str, str],
    default_row: int,
    cursor: int,
) -> int:
    current_cursor = cursor
    for child in parent:
        tag = child.tag.lower()
        if tag == "style":
            block_style = merge_styles(
                inherited_style, parse_style_string(child.get("style"))
            )
            current_cursor = _render_row_content(
                ws=ws,
                parent=child,
                inherited_style=block_style,
                default_row=default_row,
                cursor=current_cursor,
            )
            continue

        if tag == "cell":
            for_cell_row = int(child.get("row") or default_row)
            has_explicit_col = bool(child.get("column") or child.get("ref"))
            fallback_col = current_cursor if not has_explicit_col else None
            _, used_column = _render_cell(
                ws,
                child,
                inherited_style,
                for_cell_row,
                fallback_col,
            )
            span_width = abs(parse_span(child.get("colspan")))
            current_cursor = used_column + span_width
        elif tag == "merge":
            cell_range = child.get("range")
            if cell_range:
                ws.merge_cells(cell_range)

    return current_cursor
