import json
import logging
from importlib.metadata import PackageNotFoundError
from importlib.metadata import version as get_version
from pathlib import Path
from typing import Annotated

import typer

from interstellar.tools import BIP39, SLIP39

PACKAGE_NAME = Path(__file__).parent.name

logging.getLogger("slip39").setLevel(logging.ERROR)


class CLI:
    """Command Line Interface for BIP39 mnemonic generation and validation."""

    def __init__(self) -> None:
        """Initialize CLI with BIP39 and SLIP39 handlers."""
        self.bip39 = BIP39()
        self.slip39 = SLIP39()

    def get_mnemos(self, filename: str) -> list[list[str]]:
        """Read mnemonics from a file.

        Args:
            filename: Path to file containing mnemonics.

        Returns:
            2D list of mnemonic parts, one row per line.
        """
        with open(filename) as f:
            return [
                [subline.strip() for subline in line.strip().split(",")]
                for line in f.readlines()
            ]

    def enforce_standard(self, standard: str) -> None:
        """Validate that standard is either 'BIP39' or 'SLIP39'.

        Args:
            standard: The standard to validate.

        Raises:
            ValueError: If standard is not 'BIP39' or 'SLIP39'.
        """
        if standard.upper() not in ["SLIP39", "BIP39"]:
            raise ValueError("Standard must be either 'SLIP39' or 'BIP39'")

    def parse_2D_list(self, value: str) -> list[list[str]]:
        """Parse a string representation of a 2D list.

        Args:
            value: Semicolon-separated rows, comma-separated columns.

        Returns:
            2D list of strings.
        """
        value = value.strip()
        if not value:
            return []
        lines = value.split(";")
        result = [line.strip().split(",") for line in lines]
        return result


app = typer.Typer(context_settings={"help_option_names": ["-h", "--help"]})
cli = CLI()
version_help = "Show the installed version."


@app.callback()
def main(
    show_version: Annotated[
        bool,
        typer.Option(
            "--version",
            "-v",
            help=version_help,
            callback=lambda value: version() if value else None,
            is_eager=True,
        ),
    ] = False,
) -> None:
    """CLI for managing cryptocurrency mnemonics using BIP39 and SLIP39 standards."""
    pass


@app.command(
    help="Split a BIP39 mnemonic into parts or SLIP39 shares for secure backup."
)
def deconstruct(
    mnemonic: Annotated[
        str, typer.Option("--mnemonic", "-m", help="BIP39 mnemonic to deconstruct")
    ] = "",
    filename: Annotated[
        str, typer.Option("--filename", "-f", help="File containing the BIP39 mnemonic")
    ] = "",
    standard: Annotated[
        str, typer.Option("--standard", "-s", help="Output format: 'BIP39' or 'SLIP39'")
    ] = "SLIP39",
    required: Annotated[
        int,
        typer.Option(
            "--required",
            "-r",
            help="Number of required shares for SLIP39 reconstruction (e.g. 2 of 3)",
        ),
    ] = 2,
    total: Annotated[
        int,
        typer.Option(
            "--total",
            "-t",
            help="Number of total shares for SLIP39 reconstruction (e.g. 3 of 3)",
        ),
    ] = 3,
    digits: Annotated[
        bool,
        typer.Option(
            "--digits", "-d", help="Output format: use digits instead of words"
        ),
    ] = False,
) -> None:
    """Split a BIP39 mnemonic into parts or SLIP39 shares."""
    cli.enforce_standard(standard)
    if not mnemonic and filename:
        try:
            mnemos = cli.get_mnemos(filename)
            if mnemos and mnemos[0]:
                mnemonic = mnemos[0][0]
        except FileNotFoundError:
            raise typer.BadParameter(f"File not found: {filename}") from None
    if not mnemonic:
        raise ValueError("Mnemonic is required")

    # Auto-detect split based on mnemonic length
    word_count = len(mnemonic.split())
    split = 2 if word_count == 24 else 1

    bip_parts = cli.bip39.deconstruct(mnemonic, split)
    if standard.upper() == "BIP39":
        output = []
        for bip_part in bip_parts:
            mnemonic_output = bip_part
            if digits:
                # Convert words to 1-indexed digits
                mnemonic_output = " ".join(
                    str(cli.bip39.map[word]) for word in bip_part.split()
                )
            output.append(
                {
                    "standard": "BIP39",
                    "mnemonic": mnemonic_output,
                    "eth_addr": cli.bip39.eth(bip_part),
                    "digits": digits,
                }
            )
        typer.echo(json.dumps(output))
        raise typer.Exit(code=0)
    else:
        total_shares: list[list[str]] = []
        for part in bip_parts:
            shares = cli.slip39.deconstruct(part, required, total)
            if digits:
                shares = [
                    " ".join(str(cli.slip39.map[word]) for word in share.split())
                    for share in shares
                ]
            total_shares.append(shares)

        output = {
            "standard": "SLIP39",
            "shares": total_shares,
            "split": split,
            "digits": digits,
        }
        typer.echo(json.dumps(output))
        raise typer.Exit(code=0)


@app.command(help="Reconstruct a BIP39 mnemonic from SLIP39 shares or BIP39 parts.")
def reconstruct(
    shares: Annotated[
        str,
        typer.Option(
            "--shares",
            "-x",
            help="SLIP39 shares to reconstruct",
            parser=cli.parse_2D_list,
        ),
    ] = "",
    filename: Annotated[
        str,
        typer.Option(
            "--filename",
            "-f",
            help="File containing the SLIP39 shares (newline separated)",
        ),
    ] = "",
    standard: Annotated[
        str, typer.Option("--standard", "-s", help="Input format: 'BIP39' or 'SLIP39'")
    ] = "SLIP39",
    digits: Annotated[
        bool,
        typer.Option(
            "--digits", "-d", help="Input format: use digits instead of words"
        ),
    ] = False,
) -> None:
    """Reconstruct a BIP39 mnemonic from SLIP39 shares or BIP39 parts."""
    cli.enforce_standard(standard)
    if not shares and filename:
        try:
            shares = cli.get_mnemos(filename)
        except FileNotFoundError:
            raise typer.BadParameter(f"File not found: {filename}") from None
    if not shares:
        raise ValueError("Shares are required")

    required = 0

    if standard.upper() == "SLIP39":
        groups = shares
        shares = []
        for gidx, group in enumerate(groups):
            if digits:
                group = [
                    " ".join(cli.slip39.words[int(idx) - 1] for idx in member.split())
                    for member in group
                ]

            required = cli.slip39.get_required(group[gidx])
            shares.append(cli.slip39.reconstruct(group))
    else:  # BIP39
        shares = [part for group in shares for part in group]
        if digits:
            # Convert 1-indexed digits back to words
            shares = [
                " ".join(cli.bip39.words[int(idx) - 1] for idx in share.split())
                for share in shares
            ]
    reconstructed = cli.bip39.reconstruct(shares)
    output = {
        "standard": "BIP39",
        "mnemonic": reconstructed,
        "eth_addr": cli.bip39.eth(reconstructed),
        "required": required,
        "digits": digits,
    }
    typer.echo(json.dumps(output))
    raise typer.Exit(code=0)


@app.command(help=version_help)
def version() -> None:
    """Display the installed package version."""
    prefix = "v"
    try:
        typer.echo(f"{prefix}{get_version(PACKAGE_NAME)}")
    except PackageNotFoundError:
        typer.echo(f"{prefix}0.0.0")
    raise typer.Exit(code=0)


if __name__ == "__main__":
    app()
