from __future__ import annotations

import time
from dataclasses import dataclass
from pathlib import Path
from typing import TYPE_CHECKING, Iterator

from brp_lib.err import BrpError

from ._public.err import Main_ErrAlreadyUpToDate

if TYPE_CHECKING:
    from . import Brp

BASE_RECONNECT_TIMEOUT = 10.0


@dataclass
class UploadProgress:
    """Progress information yielded during a reader update.

    Attributes:
        transferred_bytes: Number of bytes transferred so far.
        estimated_total_bytes: Estimated total bytes, or None if unknown.
        estimated_time_overhead: Estimated overhead in ms beyond transfer, or None if unknown.
        reconnecting: True if reader is about to reconnect.
    """

    transferred_bytes: int
    estimated_total_bytes: int | None
    estimated_time_overhead: int | None
    reconnecting: bool

    @property
    def progress(self) -> float:
        """Progress as a float from 0.0 to 1.0. Returns 0.0 if total is unknown."""
        if not self.estimated_total_bytes:
            return 0.0
        return min(self.transferred_bytes / self.estimated_total_bytes, 1.0)


def _reconnect(brp: Brp, timeout_s: float) -> None:
    brp.close()
    deadline = time.monotonic() + BASE_RECONNECT_TIMEOUT + timeout_s
    dev_was_unregistered = False
    while True:
        if time.monotonic() > deadline:
            raise TimeoutError("Reader did not reconnect in time")
        try:
            brp.open()
        except BrpError:
            dev_was_unregistered = True
        else:
            if dev_was_unregistered:
                # brp.Sys_GetBootStatus()
                return
            brp.close()


class ReaderUpdater:
    """Uploads firmware or configuration to a BALTECH reader.

    Create via :meth:`from_path`, :meth:`from_string`, or pass a raw blob
    to the constructor. Then call :meth:`run` for a simple upload or
    :meth:`iter` to get progress updates.
    """

    @classmethod
    def from_path(cls, path: str | Path) -> ReaderUpdater:
        """Load and parse a BEC2/BF3 hex file from the given path."""
        return cls.from_string(Path(path).read_text())

    @classmethod
    def from_string(cls, hex_content: str) -> ReaderUpdater:
        """Parse BEC2/BF3 hex string content.

        The format has header lines followed by an empty line, then
        hex-encoded payload bytes. Header lines are skipped.
        """
        lines = hex_content.splitlines()
        # Skip header lines until empty line
        payload_start = 0
        for i, line in enumerate(lines):
            if line.strip() == '':
                payload_start = i + 1
                break
        else:
            # No empty line found - treat entire content as header, no payload
            return cls(b'')

        # Decode hex pairs from remaining lines
        hex_chars = ''.join(lines[payload_start:]).replace('\n', '').replace('\r', '').replace(' ', '')
        if not hex_chars:
            return cls(b'')
        return cls(bytes.fromhex(hex_chars))

    def __init__(self, blob: bytes) -> None:
        """Create from raw binary blob."""
        self._blob = blob

    def run(self, brp: Brp) -> None:
        """Upload to reader, blocking until complete.

        Equivalent to exhausting :meth:`iter` without inspecting progress.
        """
        for _ in self.iter(brp):
            pass

    def iter(self, brp: Brp) -> Iterator[UploadProgress]:
        """Upload to reader, yielding :class:`UploadProgress` after each chunk.

        Handles reconnect internally. Raises ``ValueError`` if the reader
        requests data beyond the blob size.
        """
        start_result = brp.Main_Bf3UploadStart(BrpTimeout=3000)
        adr = start_result.ReqDataAdr
        length = start_result.ReqDataLen
        transferred = 0
        estimated_total: int | None = None
        time_overhead: int | None = None

        while True:
            if adr + length > len(self._blob):
                raise ValueError(
                    f"Reader requested data at offset {adr}+{length} "
                    f"but blob is only {len(self._blob)} bytes"
                )
            transferred += length

            try:
                cont_result = brp.Main_Bf3UploadContinue(
                    DataAdr=adr,
                    Data=self._blob[adr:adr + length],
                )
            except Main_ErrAlreadyUpToDate:
                return

            if cont_result.EstimatedNumberOfBytes is not None:
                estimated_total = cont_result.EstimatedNumberOfBytes
                time_overhead = cont_result.EstimatedTimeOverhead

            yield UploadProgress(transferred, estimated_total, time_overhead, cont_result.Reconnect)

            if cont_result.Reconnect:
                timeout = cont_result.ReconnectRetryTimeout or 0
                _reconnect(brp, timeout / 1000)

            if not cont_result.Continue:
                return

            adr = cont_result.ReqDataAdr
            length = cont_result.ReqDataLen
