"""
Flux service provider using the Insight API (bitcore-node compatible).

Primary endpoint: explorer.runonflux.io (same backend as SSP wallet).
"""

from __future__ import annotations

import asyncio
import logging
from decimal import Decimal

import httpx

from fluxwallet.config.config import TIMEOUT_REQUESTS
from fluxwallet.networks import Network
from fluxwallet.transactions import FluxTransaction, Input, Output

_logger = logging.getLogger(__name__)

PROVIDERNAME = "flux"

# /api/addrs/<addr>/txs supports from/to ranges up to 500.
_MAX_RANGE = 500
# Number of concurrent range requests per round.
_CONCURRENCY = 10


class ClientError(Exception):
    """Error raised by the Flux service client."""

    def __init__(self, msg: str = ""):
        self.msg = msg
        _logger.warning(msg)

    def __str__(self) -> str:
        return self.msg


class FluxClient:
    """Async client for the Flux Insight API.

    Uses the same Insight-compatible API as the SSP wallet
    (explorer.runonflux.io). All methods are async.
    """

    def __init__(
        self,
        network: str,
        base_url: str,
        denominator: int,
        api_key: str = "",
        provider_coin_id: str = "",
        network_overrides: dict | None = None,
        timeout: int = TIMEOUT_REQUESTS,
        latest_block: int | None = None,
        strict: bool = True,
    ):
        if not isinstance(network, Network):
            self.network = Network(network)
        else:
            self.network = network
        self.base_url = base_url.rstrip("/")
        self.provider = PROVIDERNAME
        self.units = denominator
        self.timeout = timeout
        self.latest_block = latest_block
        self._client = httpx.AsyncClient(
            timeout=timeout,
            headers={"Accept": "application/json"},
        )

    # --- HTTP helpers ---

    async def _get(self, path: str) -> dict | list | int | str:
        """GET request to the Insight API.

        :param path: API path (appended to base_url/api)
        :returns: Parsed JSON response
        :raises ClientError: On HTTP errors or connection failures
        """
        url = f"{self.base_url}/api{path}"
        try:
            response = await self._client.get(url)
            response.raise_for_status()
            return response.json()
        except httpx.HTTPStatusError as e:
            msg = str(e)
            try:
                msg = e.response.text
            except Exception:
                pass
            raise ClientError(f"HTTP {e.response.status_code} from {url}: {msg}")
        except httpx.RequestError as e:
            raise ClientError(f"Request failed for {url}: {e}")

    async def _post(self, path: str, data: dict) -> dict:
        """POST request to the Insight API.

        :param path: API path (appended to base_url/api)
        :param data: JSON body
        :returns: Parsed JSON response
        :raises ClientError: On HTTP errors or connection failures
        """
        url = f"{self.base_url}/api{path}"
        try:
            response = await self._client.post(url, json=data)
            response.raise_for_status()
            return response.json()
        except httpx.HTTPStatusError as e:
            msg = str(e)
            try:
                msg = e.response.text
            except Exception:
                pass
            raise ClientError(f"HTTP {e.response.status_code} from {url}: {msg}")
        except httpx.RequestError as e:
            raise ClientError(f"Request failed for {url}: {e}")

    async def _get_tx_range(
        self, address: str, start: int, end: int
    ) -> list[dict]:
        """Fetch a range of transactions for an address.

        Uses /api/addrs/<addr>/txs?from=X&to=Y which supports ranges up to 500
        and returns transactions newest-first.

        :param address: Flux address
        :param start: Start index (inclusive, 0 = newest)
        :param end: End index (exclusive)
        :returns: List of raw transaction dicts
        """
        result = await self._get(
            f"/addrs/{address}/txs?from={start}&to={end}&noAsm=1&noScriptSig=1"
        )
        return result.get("items", [])

    # --- Blockchain queries ---

    async def blockcount(self) -> int:
        """Get the current block height.

        Uses /api/sync which returns {height, status, ...}.

        :returns: Current block height
        """
        result = await self._get("/sync")
        height = result.get("height")
        if height is None:
            raise ClientError("No height in /api/sync response")
        return int(height)

    async def getblock(
        self, block_hash: str, parse_transactions: bool = False
    ) -> dict:
        """Get block data by hash.

        :param block_hash: Block hash
        :param parse_transactions: If True, parse transaction objects
        :returns: Block data dict
        """
        return await self._get(f"/block/{block_hash}")

    async def getblockhash(self, height: int) -> str:
        """Get block hash by height.

        :param height: Block height
        :returns: Block hash string
        """
        result = await self._get(f"/block-index/{height}")
        return result["blockHash"]

    # --- Address queries ---

    async def getbalance(self, addresslist: str | list[str]) -> int:
        """Get balance for an address in satoshis.

        :param addresslist: Single address string or list of addresses
        :returns: Balance in satoshis
        """
        if isinstance(addresslist, list):
            total = 0
            for addr in addresslist:
                total += await self.getbalance(addr)
            return total
        result = await self._get(f"/addr/{addresslist}/balance")
        return int(result)

    async def getutxos(
        self,
        address: str,
        after_txid: str = "",
        limit: int = 0,
    ) -> list[dict]:
        """Get unspent transaction outputs for an address.

        :param address: Flux address (t1... or t3...)
        :param after_txid: Only return UTXOs after this txid (unused, for API compat)
        :param limit: Maximum number of UTXOs to return (0 = unlimited)
        :returns: List of UTXO dicts in wallet-expected format
        """
        raw_utxos = await self._get(f"/addr/{address}/utxo")
        if not isinstance(raw_utxos, list):
            raise ClientError(f"Unexpected UTXO response type: {type(raw_utxos)}")

        utxos = []
        for u in raw_utxos:
            utxos.append({
                "address": u.get("address", address),
                "txid": u["txid"],
                "output_n": u["vout"],
                "value": u["satoshis"],
                "script": u.get("scriptPubKey", ""),
                "confirmations": u.get("confirmations", 0),
            })
            if limit and len(utxos) >= limit:
                break

        return utxos

    async def getutxos_multi(
        self,
        addresses: list[str],
        limit: int = 0,
    ) -> list[dict]:
        """Get UTXOs for multiple addresses in a single API call.

        Uses /api/addrs/addr1,addr2,.../utxo which is far more efficient
        than individual address queries.

        :param addresses: List of Flux addresses
        :param limit: Maximum UTXOs to return (0 = unlimited)
        :returns: List of UTXO dicts in wallet-expected format
        """
        if not addresses:
            return []

        _BATCH_SIZE = 50
        utxos = []

        for i in range(0, len(addresses), _BATCH_SIZE):
            batch_addrs = addresses[i : i + _BATCH_SIZE]
            addr_str = ",".join(batch_addrs)
            raw_utxos = await self._get(f"/addrs/{addr_str}/utxo")
            if not isinstance(raw_utxos, list):
                raise ClientError(f"Unexpected UTXO response type: {type(raw_utxos)}")

            for u in raw_utxos:
                utxos.append({
                    "address": u.get("address", ""),
                    "txid": u["txid"],
                    "output_n": u["vout"],
                    "value": u["satoshis"],
                    "script": u.get("scriptPubKey", ""),
                    "confirmations": u.get("confirmations", 0),
                })
                if limit and len(utxos) >= limit:
                    return utxos

        return utxos

    # --- Transaction queries ---

    async def gettransaction(self, txid: str) -> FluxTransaction:
        """Get a transaction by txid.

        Fetches the raw hex and parses it into a FluxTransaction object.

        :param txid: Transaction ID
        :returns: Parsed FluxTransaction
        """
        # Get decoded tx for metadata
        tx_data = await self._get(f"/tx/{txid}")

        # Get raw hex for proper parsing
        raw_data = await self._get(f"/rawtx/{txid}")
        raw_hex = raw_data.get("rawtx", "")

        if raw_hex:
            t = FluxTransaction.parse_hex(
                raw_hex,
                network=self.network.name,
            )
        else:
            # Fallback: construct from decoded data
            t = self._build_transaction_from_insight(tx_data)

        t.txid = txid
        t.date = None
        t.confirmations = tx_data.get("confirmations", 0)
        t.status = "confirmed" if t.confirmations else "unconfirmed"
        t.block_height = tx_data.get("blockheight")
        t.fee = int(Decimal(str(tx_data.get("fees", 0))) * self.units)

        return t

    async def gettransactions(
        self,
        address: str,
        after_txid: str = "",
        limit: int = 50,
    ) -> list[FluxTransaction]:
        """Get transactions for an address.

        :param address: Flux address
        :param after_txid: Only return transactions after this txid
        :param limit: Maximum number of transactions
        :returns: List of FluxTransaction objects
        """
        transactions = []
        page = 0
        found_after = not bool(after_txid)

        while True:
            result = await self._get(f"/txs/?address={address}&pageNum={page}")
            txs = result.get("txs", [])
            if not txs:
                break

            for tx_data in txs:
                txid = tx_data.get("txid", "")
                if not found_after:
                    if txid == after_txid:
                        found_after = True
                    continue

                transactions.append(self._parse_tx_data(tx_data))

                if len(transactions) >= limit:
                    return transactions

            pages_total = result.get("pagesTotal", 1)
            page += 1
            if page >= pages_total:
                break

        return transactions

    async def get_transactions_by_address(
        self,
        address: str,
        after_tx_index: int = 0,
        limit: int = 0,
    ):
        """Async generator yielding transaction batches for an address.

        Uses /api/addrs/<addr>/txs with range-based pagination (from/to) for
        efficient bulk fetching. Ranges of up to 500 txs are fetched
        concurrently in rounds.

        Transactions are ordered newest-first. ``after_tx_index`` is the number
        of transactions already known (i.e. skip the first N).

        :param address: Flux address
        :param after_tx_index: Skip this many newest transactions
        :param limit: Maximum new transactions to return (0 = all)
        :yields: List[FluxTransaction] per concurrent batch
        """
        # Probe to get total transaction count.
        probe = await self._get(
            f"/addrs/{address}/txs?from=0&to=1&noAsm=1&noScriptSig=1"
        )
        total_items = probe.get("totalItems", 0)

        new_txs = total_items - after_tx_index
        if new_txs <= 0:
            return

        if limit:
            new_txs = min(new_txs, limit)

        # Build (from, to) windows covering the range we need.
        # Index 0 is newest. We want indices [0 .. new_txs), which maps to
        # the transactions the caller hasn't seen yet.
        windows = []
        pos = 0
        while pos < new_txs:
            end = min(pos + _MAX_RANGE, new_txs)
            windows.append((pos, end))
            pos = end

        # Fire all requests, stream results as they complete.
        sem = asyncio.Semaphore(_CONCURRENCY)

        async def _fetch_window(start: int, end: int) -> list:
            async with sem:
                return await self._get_tx_range(address, start, end)

        pending = [
            asyncio.create_task(_fetch_window(s, e)) for s, e in windows
        ]
        total_yielded = 0

        for coro in asyncio.as_completed(pending):
            try:
                result = await coro
            except Exception as e:
                _logger.warning(
                    "Failed to fetch tx range for %s: %s", address, e
                )
                continue

            batch = [self._parse_tx_data(tx_data) for tx_data in result]
            total_yielded += len(batch)

            if batch:
                yield batch

            if limit and total_yielded >= limit:
                for task in pending:
                    task.cancel()
                return

    # --- Transaction parsing ---

    def _parse_tx_data(self, tx_data: dict) -> FluxTransaction:
        """Parse a single Insight API tx dict into a FluxTransaction.

        Shared by gettransactions and get_transactions_by_address.
        """
        t = self._build_transaction_from_insight(tx_data)
        t.txid = tx_data.get("txid", "")
        t.confirmations = tx_data.get("confirmations", 0)
        t.status = "confirmed" if t.confirmations else "unconfirmed"
        t.block_height = tx_data.get("blockheight")
        t.fee = int(Decimal(str(tx_data.get("fees", 0))) * self.units)
        return t

    def _build_transaction_from_insight(self, tx_data: dict) -> FluxTransaction:
        """Build a FluxTransaction from Insight API decoded transaction data.

        :param tx_data: Decoded transaction dict from /api/tx/:txid
        :returns: FluxTransaction object
        """
        inputs = []
        for vin in tx_data.get("vin", []):
            inp = Input(
                prev_txid=vin.get("txid", ""),
                output_n=vin.get("vout", 0),
                value=int(Decimal(str(vin.get("value", 0))) * self.units),
                address=vin.get("addr", ""),
                witness_type="legacy",
            )
            inputs.append(inp)

        outputs = []
        for vout in tx_data.get("vout", []):
            script_pub_key = vout.get("scriptPubKey", {})
            addresses = script_pub_key.get("addresses", [])
            addr = addresses[0] if addresses else ""
            value = int(Decimal(str(vout.get("value", 0))) * self.units)
            out = Output(
                value=value,
                address=addr,
                lock_script=bytes.fromhex(script_pub_key.get("hex", "")),
                network=self.network.name,
                output_n=vout.get("n", 0),
            )
            outputs.append(out)

        return FluxTransaction(
            inputs=inputs,
            outputs=outputs,
            network=self.network.name,
            version=tx_data.get("version", 4),
            locktime=tx_data.get("locktime", 0),
        )

    # --- Broadcasting ---

    async def sendrawtransaction(self, rawtx: str) -> dict[str, str]:
        """Broadcast a raw transaction.

        :param rawtx: Transaction hex string
        :returns: Dict with 'txid' key
        :raises ClientError: If broadcast fails
        """
        result = await self._post("/tx/send", {"rawtx": rawtx})
        txid = result.get("txid")
        if not txid:
            raise ClientError(f"No txid in broadcast response: {result}")
        return {"txid": txid}

    # --- Fee estimation ---

    async def estimatefee(self, blocks: int = 1) -> int:
        """Estimate fee rate for Flux transactions.

        Flux uses a fixed fee rate of 1 sat/byte.

        :param blocks: Target confirmation blocks (unused for Flux)
        :returns: Fee rate in satoshis per KB
        """
        return 1000  # 1 sat/byte = 1000 sat/KB

    # --- Cleanup ---

    async def close(self) -> None:
        """Close the underlying HTTP client."""
        await self._client.aclose()
