from __future__ import annotations

import datetime as dt
from pathlib import Path
from typing import IO, Any
from xml.etree.ElementTree import Element

from defusedxml import ElementTree
from openpyxl.utils import column_index_from_string

XmlSource = str | Path | IO[str] | IO[bytes]


def load_xml_root(xml_source: XmlSource) -> Element:
    data = xml_source.read() if hasattr(xml_source, "read") else xml_source
    text = data.decode() if isinstance(data, (bytes, bytearray)) else str(data)

    stripped = text.lstrip()
    text_candidate = text.strip()
    if text_candidate and not stripped.startswith("<"):
        potential_path = Path(text_candidate)
        if potential_path.is_file():
            text = potential_path.read_text()

    return ElementTree.fromstring(text)


def parse_column(value: str) -> int:
    cleaned = value.strip().upper().replace("$", "")
    if not cleaned:
        raise ValueError("Column reference cannot be empty")

    if cleaned.isdigit():
        return int(cleaned)
    return int(column_index_from_string(cleaned))


def parse_column_range(value: str) -> tuple[int, int]:
    parts = [part.strip() for part in value.split(":") if part.strip()]
    if len(parts) != 2:
        raise ValueError("Column range must be in 'start:end' format")
    start = parse_column(parts[0])
    end = parse_column(parts[1])
    if end < start:
        start, end = end, start
    return start, end


def parse_span(value: str | None) -> int:
    if value is None:
        return 1
    try:
        span = int(value)
    except ValueError as exc:
        raise ValueError("Span must be an integer") from exc
    if span == 0:
        return 1
    return span


def compute_span_bounds(base: int, span: int) -> tuple[int, int]:
    if span > 0:
        start = base
        end = base + span - 1
    else:
        start = base + span + 1
        end = base

    if start < 1 or end < 1:
        raise ValueError("Span extends beyond the worksheet boundary")
    if start > end:
        start, end = end, start
    return start, end


def parse_bool(value: str | None, *, default: bool) -> bool:
    if value is None:
        return default
    low = value.strip().lower()
    if low in {"1", "true", "yes", "on"}:
        return True
    if low in {"0", "false", "no", "off"}:
        return False
    return default


def coerce_value(text_value: str | None, value_type: str | None) -> Any:
    if text_value is None:
        return None

    stripped = text_value.strip()
    if not stripped and not value_type:
        return None

    if not value_type or value_type.lower() in {"str", "string", "text"}:
        return stripped

    vt = value_type.lower()
    if vt in {"int", "integer"}:
        return int(stripped or "0")
    if vt in {"float", "number", "decimal"}:
        return float(stripped or "0")
    if vt in {"bool", "boolean"}:
        return parse_bool(value=stripped, default=False)
    if vt == "date":
        return dt.date.fromisoformat(stripped)
    if vt in {"datetime", "timestamp"}:
        return dt.datetime.fromisoformat(stripped)

    return stripped
