"""
CLI Interface for German IBAN Generator

Provides command-line interface with argument parsing and output formatting.

Copyright (c) 2025 Sebastian Wallat

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""

import click
import csv
import json
import sys
import os
import platform
import xml.etree.ElementTree as ET
from pathlib import Path
from typing import List, Optional
from click_option_group import optgroup

from .iban_generator import IBANGenerator, IBANRecord, GeneratorConfig, LegalEntity
from .downloader import BundesbankDownloader
from .config_manager import (
    get_default_config_path,
    load_config_from_file,
    write_default_config_file,
    load_full_config,
)

try:
    from importlib.metadata import version

    __version__ = version("gen-ibans")
except ImportError:
    # Fallback for older Python versions
    import pkg_resources

    __version__ = pkg_resources.get_distribution("gen-ibans").version


def _default_config_path() -> Path:
    """Return OS-specific default config file path."""
    system = platform.system().lower()
    if system == "windows":
        appdata = os.environ.get("APPDATA") or os.path.expanduser("~\\AppData\\Roaming")
        base = Path(appdata)
        return base / "gen-ibans" / "config.toml"
    elif system == "darwin":
        base = Path.home() / "Library" / "Application Support"
        return base / "gen-ibans" / "config.toml"
    else:
        # Linux and others
        base = Path(os.environ.get("XDG_CONFIG_HOME", str(Path.home() / ".config")))
        return base / "gen-ibans" / "config.toml"


def _load_config_from_file(config_path: Optional[Path] = None) -> dict:
    """Load configuration from JSON file if it exists.

    Returns a dict with keys matching GeneratorConfig fields.
    Unknown keys are ignored. If file does not exist or fails to parse,
    returns an empty dict.
    """
    path = config_path or _default_config_path()
    try:
        if not path.exists():
            return {}
        with open(path, "r", encoding="utf-8") as f:
            data = json.load(f)
        # Expect structure { "generator": { ... }, "_descriptions": { ... } }
        gen = data.get("generator", {}) if isinstance(data, dict) else {}
        # Only allow known keys
        allowed = {
            "account_holder_distribution",
            "beneficial_owner_distribution",
            "legal_entity_probability",
            "beneficiary_legal_entity_probability",
            "economically_active_probability",
            "wid_feature_distribution",
            "person_reuse_distribution",
        }
        return {k: v for k, v in gen.items() if k in allowed}
    except Exception:
        return {}


def _default_config_content() -> dict:
    """Return the default configuration content with descriptions."""
    return {
        "_descriptions": {
            "legal_entity_probability": "Wahrscheinlichkeit, dass der Kontoinhaber eine juristische Person (Firma) ist (0..1).",
            "beneficiary_legal_entity_probability": "Wahrscheinlichkeit, dass ein wirtschaftlich Berechtigter eine juristische Person ist (derzeit 0).",
            "economically_active_probability": "Wahrscheinlichkeit, dass natürliche Personen wirtschaftlich tätig sind (0..1).",
            "account_holder_distribution": "Verteilung der Anzahl der Kontoinhaber als Liste von [max_anzahl, wahrscheinlichkeit], Summe = 1.0.",
            "beneficial_owner_distribution": "Verteilung der Anzahl wirtschaftlich Berechtigter als Liste von [max_anzahl, wahrscheinlichkeit], Summe = 1.0.",
            "wid_feature_distribution": "Verteilung des WID Unterscheidungsmerkmals (nur für natürliche Personen) als Liste von [max_wert, wahrscheinlichkeit].",
            "person_reuse_distribution": "Verteilung zur Wiederverwendung von Personen (wie oft dieselbe Person in Datensätzen vorkommt) als Liste von [max_anzahl, wahrscheinlichkeit].",
        },
        "generator": {
            "account_holder_distribution": [
                [1, 0.70],
                [2, 0.15],
                [10, 0.14],
                [100, 0.009],
                [1000, 0.001],
            ],
            "beneficial_owner_distribution": [
                [0, 0.70],
                [1, 0.20],
                [2, 0.05],
                [10, 0.04],
                [50, 0.009],
                [1000, 0.001],
            ],
            "legal_entity_probability": 0.05,
            "beneficiary_legal_entity_probability": 0.0,
            "economically_active_probability": 0.20,
            "wid_feature_distribution": [
                [1, 0.80],
                [10, 0.15],
                [100, 0.04],
                [99999, 0.01],
            ],
            "person_reuse_distribution": [
                [1, 0.8],
                [2, 0.1],
                [5, 0.05],
                [15, 0.03],
                [50, 0.019],
                [200, 0.001],
            ],
        },
    }


def _write_default_config_file(target: Optional[Path] = None) -> Path:
    """Create the default config file at target or default path, returning the path."""
    path = target or _default_config_path()
    path.parent.mkdir(parents=True, exist_ok=True)
    content = _default_config_content()
    with open(path, "w", encoding="utf-8") as f:
        json.dump(content, f, ensure_ascii=False, indent=2)
    return path


class OutputFormatter:
    """Handles different output formats for generated IBANs."""

    @staticmethod
    def format_stdout(
        ibans: List[IBANRecord],
        include_bank_info: bool = True,
        include_personal_info: bool = True,
    ) -> None:
        """Output IBANs to STDOUT."""
        for record in ibans:
            # Format account holders
            holders_str = []
            for holder in record.account_holders:
                if isinstance(holder, LegalEntity):
                    holders_str.append(
                        f"{holder.name} (Legal Entity, WID: {holder.wid})"
                    )
                else:
                    ids_str = f"Tax-ID: {holder.tax_id}"
                    if holder.wid:
                        ids_str += f", WID: {holder.wid}"
                    holders_str.append(f"{holder.full_name} ({ids_str})")
            holders_display = "; ".join(holders_str)

            # Format beneficiaries
            beneficiaries_str = []
            for beneficiary in record.beneficiaries:
                if isinstance(beneficiary, LegalEntity):
                    beneficiaries_str.append(
                        f"{beneficiary.name} (Legal Entity, WID: {beneficiary.wid})"
                    )
                else:
                    ids_str = f"Tax-ID: {beneficiary.tax_id}"
                    if beneficiary.wid:
                        ids_str += f", WID: {beneficiary.wid}"
                    beneficiaries_str.append(f"{beneficiary.full_name} ({ids_str})")
            beneficiaries_display = (
                "; ".join(beneficiaries_str) if beneficiaries_str else "None"
            )

            if include_personal_info and include_bank_info:
                print(
                    f"{record.iban} | Holders: {holders_display} | Beneficiaries: {beneficiaries_display} | {record.bank.name} | {record.bank.bic} | {record.bank.bankleitzahl}"
                )
            elif include_personal_info:
                print(
                    f"{record.iban} | Holders: {holders_display} | Beneficiaries: {beneficiaries_display}"
                )
            elif include_bank_info:
                print(
                    f"{record.iban} | {record.bank.name} | {record.bank.bic} | {record.bank.bankleitzahl}"
                )
            else:
                print(record.iban)

    @staticmethod
    def format_txt(
        ibans: List[IBANRecord],
        output_path: str,
        include_bank_info: bool = True,
        include_personal_info: bool = True,
        clean: bool = False,
    ) -> None:
        """Output IBANs to a text file."""
        try:
            with open(output_path, "w", encoding="utf-8") as f:
                for record in ibans:
                    # Format account holders
                    holders_str = []
                    for holder in record.account_holders:
                        if isinstance(holder, LegalEntity):
                            holders_str.append(
                                f"{holder.name} (Legal Entity, WID: {holder.wid})"
                            )
                        else:
                            ids_str = f"Tax-ID: {holder.tax_id}"
                            if holder.wid:
                                ids_str += f", WID: {holder.wid}"
                            holders_str.append(f"{holder.full_name} ({ids_str})")
                    holders_display = "; ".join(holders_str)

                    # Format beneficiaries
                    beneficiaries_str = []
                    for beneficiary in record.beneficiaries:
                        if isinstance(beneficiary, LegalEntity):
                            beneficiaries_str.append(
                                f"{beneficiary.name} (Legal Entity, WID: {beneficiary.wid})"
                            )
                        else:
                            ids_str = f"Tax-ID: {beneficiary.tax_id}"
                            if beneficiary.wid:
                                ids_str += f", WID: {beneficiary.wid}"
                            beneficiaries_str.append(
                                f"{beneficiary.full_name} ({ids_str})"
                            )
                    beneficiaries_display = (
                        "; ".join(beneficiaries_str) if beneficiaries_str else "None"
                    )

                    if include_personal_info and include_bank_info:
                        f.write(
                            f"{record.iban} | Holders: {holders_display} | Beneficiaries: {beneficiaries_display} | {record.bank.name} | {record.bank.bic} | {record.bank.bankleitzahl}\n"
                        )
                    elif include_personal_info:
                        f.write(
                            f"{record.iban} | Holders: {holders_display} | Beneficiaries: {beneficiaries_display}\n"
                        )
                    elif include_bank_info:
                        f.write(
                            f"{record.iban} | {record.bank.name} | {record.bank.bic} | {record.bank.bankleitzahl}\n"
                        )
                    else:
                        f.write(f"{record.iban}\n")
            if not clean:
                print(f"IBANs written to: {output_path}")
        except Exception as e:
            print(f"Error writing to file {output_path}: {e}", file=sys.stderr)
            sys.exit(1)

    @staticmethod
    def format_csv(
        ibans: List[IBANRecord], output_path: str, clean: bool = False
    ) -> None:
        """Output IBANs to a CSV file."""
        try:
            with open(output_path, "w", newline="", encoding="utf-8") as f:
                writer = csv.writer(f)
                # Write header
                writer.writerow(
                    [
                        "IBAN",
                        "Account Holders",
                        "Beneficial Owners",
                        "Bank Name",
                        "BIC",
                        "Bank Code",
                    ]
                )

                # Write data
                for record in ibans:
                    # Format account holders
                    holders_str = []
                    for holder in record.account_holders:
                        if isinstance(holder, LegalEntity):
                            holders_str.append(
                                f"{holder.name} (Legal Entity, WID: {holder.wid})"
                            )
                        else:
                            ids_str = f"Tax-ID: {holder.tax_id}"
                            if holder.wid:
                                ids_str += f", WID: {holder.wid}"
                            holders_str.append(f"{holder.full_name} ({ids_str})")
                    holders_csv = "; ".join(holders_str)

                    # Format beneficiaries
                    beneficiaries_str = []
                    for beneficiary in record.beneficiaries:
                        if isinstance(beneficiary, LegalEntity):
                            beneficiaries_str.append(
                                f"{beneficiary.name} (Legal Entity, WID: {beneficiary.wid})"
                            )
                        else:
                            ids_str = f"Tax-ID: {beneficiary.tax_id}"
                            if beneficiary.wid:
                                ids_str += f", WID: {beneficiary.wid}"
                            beneficiaries_str.append(
                                f"{beneficiary.full_name} ({ids_str})"
                            )
                    beneficiaries_csv = (
                        "; ".join(beneficiaries_str) if beneficiaries_str else "None"
                    )

                    writer.writerow(
                        [
                            record.iban,
                            holders_csv,
                            beneficiaries_csv,
                            record.bank.name,
                            record.bank.bic,
                            record.bank.bankleitzahl,
                        ]
                    )

            if not clean:
                print(f"IBANs written to CSV: {output_path}")
        except Exception as e:
            print(f"Error writing to CSV file {output_path}: {e}", file=sys.stderr)
            sys.exit(1)

    @staticmethod
    def format_xml(
        ibans: List[IBANRecord], output_path: str, clean: bool = False
    ) -> None:
        """Output IBANs to an XML file."""
        try:
            root = ET.Element("accounts")

            for record in ibans:
                iban_elem = ET.SubElement(root, "account")
                ET.SubElement(iban_elem, "iban").text = record.iban

                # Account holders
                holders_elem = ET.SubElement(iban_elem, "account_holders")
                for holder in record.account_holders:
                    holder_elem = ET.SubElement(holders_elem, "holder")
                    if isinstance(holder, LegalEntity):
                        ET.SubElement(holder_elem, "type").text = "legal_entity"
                        ET.SubElement(holder_elem, "name").text = holder.name
                        ET.SubElement(holder_elem, "wid").text = holder.wid
                        ET.SubElement(
                            holder_elem, "street_address"
                        ).text = holder.street_address
                        ET.SubElement(holder_elem, "city").text = holder.city
                        ET.SubElement(
                            holder_elem, "postal_code"
                        ).text = holder.postal_code
                    else:
                        ET.SubElement(holder_elem, "type").text = "natural_person"
                        ET.SubElement(
                            holder_elem, "first_name"
                        ).text = holder.first_name
                        ET.SubElement(holder_elem, "last_name").text = holder.last_name
                        ET.SubElement(holder_elem, "birth_date").text = str(
                            holder.birth_date
                        )
                        ET.SubElement(holder_elem, "tax_id").text = holder.tax_id
                        if holder.wid:
                            ET.SubElement(holder_elem, "wid").text = holder.wid
                        ET.SubElement(
                            holder_elem, "street_address"
                        ).text = holder.street_address
                        ET.SubElement(holder_elem, "city").text = holder.city
                        ET.SubElement(
                            holder_elem, "postal_code"
                        ).text = holder.postal_code

                # Beneficiaries
                beneficiaries_elem = ET.SubElement(iban_elem, "beneficiaries")
                for beneficiary in record.beneficiaries:
                    beneficiary_elem = ET.SubElement(beneficiaries_elem, "beneficiary")
                    if isinstance(beneficiary, LegalEntity):
                        ET.SubElement(beneficiary_elem, "type").text = "legal_entity"
                        ET.SubElement(beneficiary_elem, "name").text = beneficiary.name
                        ET.SubElement(beneficiary_elem, "wid").text = beneficiary.wid
                        ET.SubElement(
                            beneficiary_elem, "street_address"
                        ).text = beneficiary.street_address
                        ET.SubElement(beneficiary_elem, "city").text = beneficiary.city
                        ET.SubElement(
                            beneficiary_elem, "postal_code"
                        ).text = beneficiary.postal_code
                    else:
                        ET.SubElement(beneficiary_elem, "type").text = "natural_person"
                        ET.SubElement(
                            beneficiary_elem, "first_name"
                        ).text = beneficiary.first_name
                        ET.SubElement(
                            beneficiary_elem, "last_name"
                        ).text = beneficiary.last_name
                        ET.SubElement(beneficiary_elem, "birth_date").text = str(
                            beneficiary.birth_date
                        )
                        ET.SubElement(
                            beneficiary_elem, "tax_id"
                        ).text = beneficiary.tax_id
                        if beneficiary.wid:
                            ET.SubElement(
                                beneficiary_elem, "wid"
                            ).text = beneficiary.wid
                        ET.SubElement(
                            beneficiary_elem, "street_address"
                        ).text = beneficiary.street_address
                        ET.SubElement(beneficiary_elem, "city").text = beneficiary.city
                        ET.SubElement(
                            beneficiary_elem, "postal_code"
                        ).text = beneficiary.postal_code

                # Bank info
                bank_elem = ET.SubElement(iban_elem, "bank")
                ET.SubElement(bank_elem, "name").text = record.bank.name
                ET.SubElement(bank_elem, "bic").text = record.bank.bic
                ET.SubElement(bank_elem, "code").text = record.bank.bankleitzahl

            # Write to file with proper formatting
            tree = ET.ElementTree(root)
            ET.indent(tree, space="  ", level=0)
            tree.write(output_path, encoding="utf-8", xml_declaration=True)

            if not clean:
                print(f"IBANs written to XML: {output_path}")
        except Exception as e:
            print(f"Error writing to XML file {output_path}: {e}", file=sys.stderr)
            sys.exit(1)

    @staticmethod
    def format_json(
        ibans: List[IBANRecord], output_path: str, clean: bool = False
    ) -> None:
        """Output IBANs to a JSON file."""
        try:
            data = []
            for record in ibans:
                # Account holders
                holders_data = []
                for holder in record.account_holders:
                    if isinstance(holder, LegalEntity):
                        holders_data.append(
                            {
                                "type": "legal_entity",
                                "name": holder.name,
                                "wid": holder.wid,
                                "street_address": holder.street_address,
                                "city": holder.city,
                                "postal_code": holder.postal_code,
                            }
                        )
                    else:
                        holder_data = {
                            "type": "natural_person",
                            "first_name": holder.first_name,
                            "last_name": holder.last_name,
                            "birth_date": str(holder.birth_date),
                            "tax_id": holder.tax_id,
                            "street_address": holder.street_address,
                            "city": holder.city,
                            "postal_code": holder.postal_code,
                        }
                        if holder.wid:
                            holder_data["wid"] = holder.wid
                        holders_data.append(holder_data)

                # Beneficiaries
                beneficiaries_data = []
                for beneficiary in record.beneficiaries:
                    if isinstance(beneficiary, LegalEntity):
                        beneficiaries_data.append(
                            {
                                "type": "legal_entity",
                                "name": beneficiary.name,
                                "wid": beneficiary.wid,
                                "street_address": beneficiary.street_address,
                                "city": beneficiary.city,
                                "postal_code": beneficiary.postal_code,
                            }
                        )
                    else:
                        beneficiary_data = {
                            "type": "natural_person",
                            "first_name": beneficiary.first_name,
                            "last_name": beneficiary.last_name,
                            "birth_date": str(beneficiary.birth_date),
                            "tax_id": beneficiary.tax_id,
                            "street_address": beneficiary.street_address,
                            "city": beneficiary.city,
                            "postal_code": beneficiary.postal_code,
                        }
                        if beneficiary.wid:
                            beneficiary_data["wid"] = beneficiary.wid
                        beneficiaries_data.append(beneficiary_data)

                data.append(
                    {
                        "iban": record.iban,
                        "account_holders": holders_data,
                        "beneficiaries": beneficiaries_data,
                        "bank": {
                            "name": record.bank.name,
                            "bic": record.bank.bic,
                            "code": record.bank.bankleitzahl,
                        },
                    }
                )

            with open(output_path, "w", encoding="utf-8") as f:
                json.dump(data, f, ensure_ascii=False, indent=2)

            if not clean:
                print(f"IBANs written to JSON: {output_path}")
        except Exception as e:
            print(f"Error writing to JSON file {output_path}: {e}", file=sys.stderr)
            sys.exit(1)


class ColoredHelpCommand(click.Command):
    """Custom Click Command that adds colors to the help output.

    In addition to standard headers, this also highlights option group headers
    (from click-option-group) to make the help more scannable.
    """

    def get_help(self, ctx: click.Context) -> str:
        text = super().get_help(ctx)
        # Determine if we should colorize help (stdout context)
        no_color = False
        try:
            no_color = bool(ctx.params.get("no_color", False))
        except Exception:
            no_color = False
        enable_color = sys.stdout.isatty() and not no_color
        if not enable_color:
            return text

        def s(t: str, fg: Optional[str] = None, bold: bool = False) -> str:
            return click.style(t, fg=fg, bold=bold)

        # Color common section headers
        for header in ("Usage:", "Options:", "Arguments:", "Commands:"):
            if header in text:
                text = text.replace(header, s(header, fg="cyan", bold=True))

        # Color option flags and option group headers
        import re

        colored_lines = []
        group_header_pattern = re.compile(r"^(\s*)([^:\n][^:\n]*:)\s*$")
        for line in text.splitlines():
            # Highlight option group headers: lines ending with ':' (possibly indented)
            m_hdr = group_header_pattern.match(line)
            if m_hdr:
                indent, hdr = m_hdr.groups()
                # Avoid double-coloring for already-colored common headers
                if hdr not in {"Usage:", "Options:", "Arguments:", "Commands:"}:
                    line = f"{indent}{s(hdr, fg='bright_cyan', bold=True)}"
            else:
                # Match lines that look like options, e.g., "  -v, --version  Show version"
                m = re.match(r"(\s*)(-[^\s,][^\s]*?(?:,\s*--[^\s]+)?)(.*)", line)
                if m:
                    indent, flags, rest = m.groups()
                    line = (
                        f"{indent}{s(flags, fg='yellow', bold=True)}{s(rest, fg=None)}"
                    )
            colored_lines.append(line)
        return "\n".join(colored_lines)


@click.command(cls=ColoredHelpCommand)
@click.argument(
    "data_file", type=click.Path(exists=True, path_type=Path), required=False
)
@click.option(
    "--seed", type=int, help="PRNG seed for deterministic generation (optional)"
)
@click.option(
    "--count", default=1, type=int, help="Number of IBANs to generate (default: 1)"
)
@click.option(
    "--format",
    "output_format",
    type=click.Choice(["txt", "csv", "xml", "json"]),
    help="Output format for file or stdout: txt, csv, xml, or json",
)
@click.option(
    "--output",
    type=click.Path(path_type=Path),
    help="Output file path (if not specified, output goes to stdout)",
)
@click.option(
    "--no-echo", is_flag=True, help="Suppress output to stdout when writing to a file"
)
@click.option(
    "--iban-only",
    is_flag=True,
    help="Output only IBANs without bank information or personal data",
)
@click.option(
    "--no-personal-info",
    is_flag=True,
    help="Exclude personal information (names and addresses) from output",
)
@click.option(
    "--no-bank-info", is_flag=True, help="Exclude bank information from output"
)
@click.option(
    "--clean",
    is_flag=True,
    help="Output only data without additional informational messages",
)
@click.option(
    "--no-color",
    is_flag=True,
    help="Deaktiviere farbige Ausgabe (nur Schwarz/Weiß)",
)
@click.option(
    "--download-format",
    type=click.Choice(["csv", "txt", "xml"]),
    default="csv",
    help="Format to download from Bundesbank when no data file is provided (default: csv)",
)
@click.option(
    "--force-download",
    is_flag=True,
    help="Force re-download even if cached data is available",
)
@click.option(
    "--cache-dir",
    type=click.Path(path_type=Path),
    help="Directory to cache downloaded data (default: system temp directory)",
)
@click.option(
    "--no-version-check",
    is_flag=True,
    help="Disable checking for newer versions online (rely only on cache age)",
)
@click.option(
    "--filter-bank-name",
    type=str,
    help="Regex to filter by bank name (case-insensitive)",
)
@click.option(
    "--filter-bic",
    type=str,
    help="Regex to filter by BIC (case-insensitive)",
)
@click.option(
    "--filter-blz",
    type=str,
    help="Regex to filter by BLZ/Bankleitzahl",
)
@optgroup.group("Entity Type Configuration")
@optgroup.option(
    "--legal-entity-probability",
    type=float,
    default=0.05,
    help="Probability of generating legal entity as sole account holder (default: 0.05 = 5%)",
)
@optgroup.group("Account Holders Distribution")
@optgroup.option(
    "--account-holder-single-prob",
    type=float,
    default=0.70,
    help="Probability of single account holder (default: 0.70 = 70%)",
)
@optgroup.option(
    "--account-holder-two-prob",
    type=float,
    default=0.15,
    help="Probability of two account holders (default: 0.15 = 15%)",
)
@optgroup.group("Beneficiaries Distribution")
@optgroup.option(
    "--beneficial-owner-zero-prob",
    type=float,
    default=0.70,
    help="Probability of zero beneficial owners (default: 0.70 = 70%)",
)
@optgroup.option(
    "--beneficial-owner-one-prob",
    type=float,
    default=0.20,
    help="Probability of one beneficial owner (default: 0.20 = 20%)",
)
@optgroup.group("Economic Activity Configuration")
@optgroup.option(
    "--economically-active-prob",
    type=float,
    default=0.20,
    help="Probability of natural persons being economically active (default: 0.20 = 20%)",
)
@optgroup.group("WID Features Distribution")
@optgroup.option(
    "--wid-feature-00001-prob",
    type=float,
    default=0.80,
    help="Probability of WID feature 00001 (default: 0.80 = 80%)",
)
@optgroup.option(
    "--wid-feature-00002-00010-prob",
    type=float,
    default=0.15,
    help="Probability of WID feature 00002-00010 (default: 0.15 = 15%)",
)
@optgroup.group("Person Reuse Distribution")
@optgroup.option(
    "--person-reuse-single-prob",
    type=float,
    default=0.90,
    help="Probability of single person use/no reuse (default: 0.90 = 90%)",
)
@optgroup.option(
    "--person-reuse-two-prob",
    type=float,
    default=0.05,
    help="Probability of person being used twice (default: 0.05 = 5%)",
)
@click.pass_context
def main(
    ctx: click.Context,
    data_file: Optional[Path],
    seed: int,
    count: int,
    output_format: str,
    output: Path,
    no_echo: bool,
    iban_only: bool,
    no_personal_info: bool,
    no_bank_info: bool,
    clean: bool,
    no_color: bool,
    download_format: str,
    force_download: bool,
    cache_dir: Optional[Path],
    no_version_check: bool,
    filter_bank_name: Optional[str],
    filter_bic: Optional[str],
    filter_blz: Optional[str],
    legal_entity_probability: float,
    account_holder_single_prob: float,
    account_holder_two_prob: float,
    beneficial_owner_zero_prob: float,
    beneficial_owner_one_prob: float,
    economically_active_prob: float,
    wid_feature_00001_prob: float,
    wid_feature_00002_00010_prob: float,
    person_reuse_single_prob: float,
    person_reuse_two_prob: float,
) -> None:
    """Generate valid German IBANs using Bundesbank data.

    Data is always displayed on the command line by default in plain format.
    Use --format to specify alternative output formats (txt, csv, xml, json).
    Use --output to write to a file. Use --no-echo to suppress stdout when writing to files.

    If no DATA_FILE is provided, the latest data will be automatically downloaded
    from the Deutsche Bundesbank website and cached locally. The system automatically
    checks for newer versions online by comparing ETags, ensuring you always have
    current data. Use --no-version-check to disable this and rely only on cache age.

    Supports CSV, TXT, and XML input file formats.

    Examples:
      # Use automatically downloaded data
      gen-ibans --count 5
      gen-ibans --count 10 --download-format xml

      # Use local data file
      gen-ibans data/blz-aktuell-csv-data.csv --count 5
      gen-ibans data/blz-aktuell-csv-data.csv --count 5 --format json
      gen-ibans data/blz-aktuell-csv-data.csv --count 5 --format txt --output ibans.txt
      gen-ibans data/blz-aktuell-csv-data.csv --count 5 --format csv --output ibans.csv --no-echo
      gen-ibans data/blz-aktuell-xml-data.xml --count 100 --format json --output ibans.json

      # Force re-download of data
      gen-ibans --count 5 --force-download
    """

    # Merge additional defaults from config file (CLI and downloader) if not provided on CLI
    merged = _merge_defaults(
        ctx,
        download_format=download_format,
        force_download=force_download,
        cache_dir=cache_dir,
        no_version_check=no_version_check,
        seed=seed,
        count=count,
        output_format=output_format,
        output=output,
        no_echo=no_echo,
        iban_only=iban_only,
        no_personal_info=no_personal_info,
        no_bank_info=no_bank_info,
        clean=clean,
        no_color=no_color,
        filter_bank_name=filter_bank_name,
        filter_bic=filter_bic,
        filter_blz=filter_blz,
    )
    download_format = merged["download_format"]
    force_download = merged["force_download"]
    cache_dir = merged["cache_dir"]
    no_version_check = merged["no_version_check"]
    seed = merged["seed"]
    count = merged["count"]
    output_format = merged["output_format"]
    output = merged["output"]
    no_echo = merged["no_echo"]
    iban_only = merged["iban_only"]
    no_personal_info = merged["no_personal_info"]
    no_bank_info = merged["no_bank_info"]
    clean = merged["clean"]
    no_color = merged["no_color"]
    # Apply filters from config if not provided on CLI
    filter_bank_name = merged.get("filter_bank_name", filter_bank_name)
    filter_bic = merged.get("filter_bic", filter_bic)
    filter_blz = merged.get("filter_blz", filter_blz)

    # Determine whether to enable colored output for CLI messages (stderr/info)
    enable_color = sys.stderr.isatty() and not no_color and not clean

    def style(text: str, fg: Optional[str] = None, bold: bool = False) -> str:
        return click.style(text, fg=fg, bold=bold) if enable_color else text

    # Validate arguments
    if count <= 0:
        raise click.BadParameter("Count must be a positive integer")

    try:
        # Determine data file to use (download if necessary)
        data_file_path = _determine_data_file_path(
            data_file=data_file,
            download_format=download_format,
            force_download=force_download,
            cache_dir=cache_dir,
            no_version_check=no_version_check,
            clean=clean,
            style=style,
        )

        # Build configuration from CLI + config file + defaults
        config = _build_generator_config(
            ctx,
            legal_entity_probability=legal_entity_probability,
            account_holder_single_prob=account_holder_single_prob,
            account_holder_two_prob=account_holder_two_prob,
            beneficial_owner_zero_prob=beneficial_owner_zero_prob,
            beneficial_owner_one_prob=beneficial_owner_one_prob,
            economically_active_prob=economically_active_prob,
            wid_feature_00001_prob=wid_feature_00001_prob,
            wid_feature_00002_00010_prob=wid_feature_00002_00010_prob,
            person_reuse_single_prob=person_reuse_single_prob,
            person_reuse_two_prob=person_reuse_two_prob,
        )

        # Initialize IBAN generator
        if not clean:
            click.echo(
                style(f"Loading bank data from: {data_file_path}", fg="cyan"), err=True
            )
        generator = IBANGenerator(data_file_path, seed, config)

        # Apply optional regex filters on bank list
        _apply_bank_filters(
            generator,
            filter_bank_name=filter_bank_name,
            filter_bic=filter_bic,
            filter_blz=filter_blz,
        )

        if not clean:
            click.echo(
                style(f"Loaded {generator.get_bank_count()} banks", fg="green"),
                err=True,
            )
            click.echo(style(f"Using seed: {generator.seed}", fg="magenta"), err=True)

        # Generate IBANs
        if not clean:
            click.echo(style(f"Generating {count} IBANs...", fg="cyan"), err=True)
        ibans = generator.generate_ibans(count)

        # Determine what information to include
        include_personal_info = not (no_personal_info or iban_only)
        include_bank_info = not (no_bank_info or iban_only)

        # Always output to stdout unless --no-echo is specified and --output is given
        output_to_stdout = not (no_echo and output)

        # Output results to stdout (all formats) or files
        if output_to_stdout:
            _output_results_stdout(
                ibans,
                output_format=output_format,
                include_bank_info=include_bank_info,
                include_personal_info=include_personal_info,
            )

        if output:
            if not output_format:
                raise click.BadParameter(
                    "--format is required when --output is specified"
                )
            formatter = OutputFormatter()
            if output_format == "txt":
                formatter.format_txt(
                    ibans, str(output), include_bank_info, include_personal_info, clean
                )
            elif output_format == "csv":
                formatter.format_csv(ibans, str(output), clean)
            elif output_format == "xml":
                formatter.format_xml(ibans, str(output), clean)
            elif output_format == "json":
                formatter.format_json(ibans, str(output), clean)

        if not clean:
            click.echo(
                style(
                    f"Successfully generated {len(ibans)} IBANs", fg="green", bold=True
                ),
                err=True,
            )

    except Exception as e:
        raise click.ClickException(f"Error: {e}")


@click.command(help="Erstellt eine Default-Konfigurationsdatei an der Standard-Stelle")
@click.option(
    "--path",
    "path_opt",
    type=click.Path(path_type=Path),
    required=False,
    help="Optionaler Zielpfad für die Konfigurationsdatei",
)
@click.pass_context
def init_config(ctx: click.Context, path_opt: Optional[Path] = None) -> None:
    """CLI-Kommando: Default-Konfiguration schreiben und Speicherort ausgeben."""
    try:
        if path_opt:
            target = Path(path_opt)
        else:
            # Use global --config-dir if provided, otherwise OS default path
            cfg_dir = (
                (ctx.obj or {}).get("config_dir") if isinstance(ctx.obj, dict) else None
            )
            if cfg_dir:
                Path(cfg_dir).mkdir(parents=True, exist_ok=True)
                target = Path(cfg_dir) / "config.toml"
            else:
                target = get_default_config_path()
        created = write_default_config_file(target)
        click.echo(f"Default-Konfiguration wurde erstellt: {created}")
    except Exception as e:
        raise click.ClickException(
            f"Konfigurationsdatei konnte nicht erstellt werden: {e}"
        )


# Define a Click group to support subcommands like `gen` and `init`
@click.group(
    cls=click.Group,
    help="gen-ibans command suite with subcommands",
    invoke_without_command=True,
)
@click.option(
    "--config-dir",
    "global_config_dir",
    type=click.Path(path_type=Path, file_okay=False),
    required=False,
    help="Global: Ordner, in dem Konfigurationsdateien gesucht/geschrieben werden (wirkt auf Unterkommandos)",
)
@click.option("-v", "--version", is_flag=True, help="Show version and exit")
@click.option(
    "--show-config-path",
    is_flag=True,
    help="Zeige den Pfad zur Konfigurationsdatei und beende",
)
@click.pass_context
def cli(
    ctx: click.Context,
    global_config_dir: Optional[Path],
    version: bool,
    show_config_path: bool,
) -> None:
    """Command group entry point."""
    # Store global options for subcommands
    ctx.ensure_object(dict)
    if global_config_dir:
        ctx.obj["config_dir"] = Path(global_config_dir)
    # Handle version at top-level
    if version and ctx.invoked_subcommand is None:
        try:
            from importlib.metadata import version as _ver

            click.echo(_ver("gen-ibans"))
        except Exception:
            click.echo(__version__)
        ctx.exit(0)
    # Handle show-config-path at top-level
    if show_config_path and ctx.invoked_subcommand is None:
        click.echo(str(get_default_config_path()))
        ctx.exit(0)


# Register subcommands
cli.add_command(main, name="gen")
cli.add_command(init_config, name="init")


# Helper functions extracted from main to improve readability


def _merge_defaults(
    ctx: click.Context,
    *,
    download_format: str,
    force_download: bool,
    cache_dir: Optional[Path],
    no_version_check: bool,
    seed: Optional[int],
    count: int,
    output_format: Optional[str],
    output: Optional[Path],
    no_echo: bool,
    iban_only: bool,
    no_personal_info: bool,
    no_bank_info: bool,
    clean: bool,
    no_color: bool,
    filter_bank_name: Optional[str],
    filter_bic: Optional[str],
    filter_blz: Optional[str],
):
    """Merge additional defaults from config file (CLI and downloader) if not provided on CLI.

    Returns a dict with possibly updated values.
    """
    try:
        provided_params = {
            param
            for param in ctx.params.keys()
            if ctx.get_parameter_source(param) != click.core.ParameterSource.DEFAULT
        }
        full_cfg = load_full_config()
        cli_cfg = full_cfg.get("cli", {}) or {}
        dl_cfg = full_cfg.get("downloader", {}) or {}
        # Downloader-related defaults
        if "download_format" not in provided_params and isinstance(
            dl_cfg.get("download_format"), str
        ):
            download_format = dl_cfg["download_format"]
        if "force_download" not in provided_params and isinstance(
            dl_cfg.get("force_download"), bool
        ):
            force_download = dl_cfg["force_download"]
        if "cache_dir" not in provided_params and dl_cfg.get("cache_dir"):
            try:
                cache_dir = (
                    Path(dl_cfg["cache_dir"]) if dl_cfg.get("cache_dir") else cache_dir
                )
            except Exception:
                pass
        if "no_version_check" not in provided_params and isinstance(
            dl_cfg.get("no_version_check"), bool
        ):
            no_version_check = dl_cfg["no_version_check"]
        # CLI-related defaults
        if "seed" not in provided_params and cli_cfg.get("seed") is not None:
            try:
                seed = int(cli_cfg["seed"])  # type: ignore
            except Exception:
                pass
        if "count" not in provided_params and isinstance(cli_cfg.get("count"), int):
            count = cli_cfg["count"]
        if "output_format" not in provided_params and cli_cfg.get("output_format"):
            output_format = cli_cfg.get("output_format")
        if "output" not in provided_params and cli_cfg.get("output"):
            try:
                output = Path(cli_cfg["output"])  # type: ignore
            except Exception:
                pass
        if "no_echo" not in provided_params and isinstance(
            cli_cfg.get("no_echo"), bool
        ):
            no_echo = cli_cfg["no_echo"]
        if "iban_only" not in provided_params and isinstance(
            cli_cfg.get("iban_only"), bool
        ):
            iban_only = cli_cfg["iban_only"]
        if "no_personal_info" not in provided_params and isinstance(
            cli_cfg.get("no_personal_info"), bool
        ):
            no_personal_info = cli_cfg["no_personal_info"]
        if "no_bank_info" not in provided_params and isinstance(
            cli_cfg.get("no_bank_info"), bool
        ):
            no_bank_info = cli_cfg["no_bank_info"]
        if "clean" not in provided_params and isinstance(cli_cfg.get("clean"), bool):
            clean = cli_cfg["clean"]
        if "no_color" not in provided_params and isinstance(
            cli_cfg.get("no_color"), bool
        ):
            no_color = cli_cfg["no_color"]
        # Filters: allow from config if not provided on CLI
        if "filter_bank_name" not in provided_params and cli_cfg.get(
            "filter_bank_name"
        ):
            try:
                # Empty strings are treated as unset
                fbn = str(cli_cfg.get("filter_bank_name"))
                filter_bank_name = fbn if fbn.strip() != "" else None  # type: ignore
            except Exception:
                pass
        if "filter_bic" not in provided_params and cli_cfg.get("filter_bic"):
            try:
                fbic = str(cli_cfg.get("filter_bic"))
                filter_bic = fbic if fbic.strip() != "" else None  # type: ignore
            except Exception:
                pass
        if "filter_blz" not in provided_params and cli_cfg.get("filter_blz"):
            try:
                fblz = str(cli_cfg.get("filter_blz"))
                filter_blz = fblz if fblz.strip() != "" else None  # type: ignore
            except Exception:
                pass
    except Exception:
        # Ignore config merge failures for non-generator settings
        pass

    return {
        "download_format": download_format,
        "force_download": force_download,
        "cache_dir": cache_dir,
        "no_version_check": no_version_check,
        "seed": seed,
        "count": count,
        "output_format": output_format,
        "output": output,
        "no_echo": no_echo,
        "iban_only": iban_only,
        "no_personal_info": no_personal_info,
        "no_bank_info": no_bank_info,
        "clean": clean,
        "no_color": no_color,
        "filter_bank_name": filter_bank_name,
        "filter_bic": filter_bic,
        "filter_blz": filter_blz,
    }


def _determine_data_file_path(
    *,
    data_file: Optional[Path],
    download_format: str,
    force_download: bool,
    cache_dir: Optional[Path],
    no_version_check: bool,
    clean: bool,
    style,
) -> str:
    """Determine the data file path, downloading if necessary."""
    if data_file is None:
        if not clean:
            click.echo(
                style(
                    f"No data file provided, downloading from Bundesbank ({download_format} format)...",
                    fg="yellow",
                ),
                err=True,
            )
        downloader = BundesbankDownloader(
            cache_dir=str(cache_dir) if cache_dir else None
        )
        try:
            data_file_path = downloader.get_data_file(
                format_type=download_format,
                force_download=force_download,
                check_version=not no_version_check,
            )
            if not clean:
                click.echo(
                    style(f"Downloaded data cached at: {data_file_path}", fg="cyan"),
                    err=True,
                )
        except Exception as e:
            raise click.ClickException(f"Failed to download Bundesbank data: {e}")
    else:
        data_file_path = str(data_file)
    return data_file_path


def _apply_bank_filters(
    generator: IBANGenerator,
    *,
    filter_bank_name: Optional[str],
    filter_bic: Optional[str],
    filter_blz: Optional[str],
) -> None:
    """Apply optional regex filters to the generator's bank list."""
    try:
        import re as _re

        filtered_banks = generator.banks
        if filter_bank_name:
            pat_name = _re.compile(filter_bank_name, _re.IGNORECASE)
            filtered_banks = [
                b for b in filtered_banks if pat_name.search(b.name or "")
            ]
        if filter_bic:
            pat_bic = _re.compile(filter_bic, _re.IGNORECASE)
            filtered_banks = [b for b in filtered_banks if pat_bic.search(b.bic or "")]
        if filter_blz:
            pat_blz = _re.compile(filter_blz)
            filtered_banks = [
                b for b in filtered_banks if pat_blz.search(b.bankleitzahl or "")
            ]
        if filter_bank_name or filter_bic or filter_blz:
            generator.banks = filtered_banks
            if not generator.banks:
                raise click.ClickException("No banks match the provided filter(s).")
    except Exception as e:
        raise click.ClickException(f"Invalid filter regex provided: {e}")


def _get_provided_params(ctx: click.Context) -> set:
    return {
        param
        for param in ctx.params.keys()
        if ctx.get_parameter_source(param) != click.core.ParameterSource.DEFAULT
    }


def _convert_pairs_list(value):
    if isinstance(value, list):
        return [
            (int(p[0]), float(p[1]))
            for p in value
            if isinstance(p, (list, tuple)) and len(p) == 2
        ]
    return value


def _apply_config_file_values(config: GeneratorConfig, values: dict) -> None:
    for key, val in values.items():
        if key in {
            "account_holder_distribution",
            "beneficial_owner_distribution",
            "wid_feature_distribution",
            "person_reuse_distribution",
        }:
            val = _convert_pairs_list(val)
        try:
            setattr(config, key, val)
        except Exception:
            # Ignore unknown/mismatched keys
            pass


def _compose_account_holder_distribution(single: float, two: float):
    ah_remaining = 1.0 - single - two
    ah_ten_prob = ah_remaining * 0.933
    ah_hundred_prob = ah_remaining * 0.06
    ah_thousand_prob = ah_remaining * 0.007
    return [
        (1, single),
        (2, two),
        (10, ah_ten_prob),
        (100, ah_hundred_prob),
        (1000, ah_thousand_prob),
    ]


def _compose_beneficial_owner_distribution(zero: float, one: float):
    bo_remaining = 1.0 - zero - one
    bo_two_prob = bo_remaining * 0.5
    bo_ten_prob = bo_remaining * 0.4
    bo_fifty_prob = bo_remaining * 0.09
    bo_thousand_prob = bo_remaining * 0.01
    return [
        (0, zero),
        (1, one),
        (2, bo_two_prob),
        (10, bo_ten_prob),
        (50, bo_fifty_prob),
        (1000, bo_thousand_prob),
    ]


def _compose_wid_feature_distribution(p00001: float, p00002_00010: float):
    wf_remaining = 1.0 - p00001 - p00002_00010
    wf_00011_00100_prob = wf_remaining * 0.8
    wf_00101_99999_prob = wf_remaining * 0.2
    return [
        (1, p00001),
        (10, p00002_00010),
        (100, wf_00011_00100_prob),
        (99999, wf_00101_99999_prob),
    ]


def _compose_person_reuse_distribution(single: float, two: float):
    pr_remaining = 1.0 - single - two
    pr_ten_prob = pr_remaining * 0.933
    pr_hundred_prob = pr_remaining * 0.06
    pr_thousand_prob = pr_remaining * 0.007
    return [
        (1, single),
        (2, two),
        (10, pr_ten_prob),
        (100, pr_hundred_prob),
        (1000, pr_thousand_prob),
    ]


def _build_generator_config(
    ctx: click.Context,
    *,
    legal_entity_probability: float,
    account_holder_single_prob: float,
    account_holder_two_prob: float,
    beneficial_owner_zero_prob: float,
    beneficial_owner_one_prob: float,
    economically_active_prob: float,
    wid_feature_00001_prob: float,
    wid_feature_00002_00010_prob: float,
    person_reuse_single_prob: float,
    person_reuse_two_prob: float,
) -> GeneratorConfig:
    """Build GeneratorConfig by merging config file values with CLI overrides."""
    # Base config from file and defaults
    config = GeneratorConfig()
    _apply_config_file_values(config, load_config_from_file())

    config_kwargs = {}
    provided_params = _get_provided_params(ctx)

    # Simple numeric overrides
    if "legal_entity_probability" in provided_params:
        config_kwargs["legal_entity_probability"] = legal_entity_probability
    if "economically_active_prob" in provided_params:
        config_kwargs["economically_active_probability"] = economically_active_prob

    # Distribution overrides (only when any related CLI value was provided)
    if any(
        p in provided_params
        for p in ("account_holder_single_prob", "account_holder_two_prob")
    ):
        config_kwargs["account_holder_distribution"] = (
            _compose_account_holder_distribution(
                account_holder_single_prob, account_holder_two_prob
            )
        )

    if any(
        p in provided_params
        for p in ("beneficial_owner_zero_prob", "beneficial_owner_one_prob")
    ):
        config_kwargs["beneficial_owner_distribution"] = (
            _compose_beneficial_owner_distribution(
                beneficial_owner_zero_prob, beneficial_owner_one_prob
            )
        )

    if any(
        p in provided_params
        for p in ("wid_feature_00001_prob", "wid_feature_00002_00010_prob")
    ):
        config_kwargs["wid_feature_distribution"] = _compose_wid_feature_distribution(
            wid_feature_00001_prob, wid_feature_00002_00010_prob
        )

    if any(
        p in provided_params
        for p in ("person_reuse_single_prob", "person_reuse_two_prob")
    ):
        config_kwargs["person_reuse_distribution"] = _compose_person_reuse_distribution(
            person_reuse_single_prob, person_reuse_two_prob
        )

    for key, val in config_kwargs.items():
        setattr(config, key, val)

    return config


def _format_person_inline(person) -> str:
    # Helper for txt/csv human-readable inline formatting
    if isinstance(person, LegalEntity):
        return f"{person.name} (Legal Entity, WID: {person.wid}), Address: {person.full_address}"
    else:
        ids_str = f"Tax-ID: {person.tax_id}"
        if getattr(person, "wid", None):
            ids_str += f", WID: {person.wid}"
        return f"{person.full_name} ({ids_str})"


def _person_to_dict(person) -> dict:
    # Helper for JSON formatting
    if isinstance(person, LegalEntity):
        return {
            "type": "legal_entity",
            "name": person.name,
            "wid": person.wid,
            "street_address": person.street_address,
            "city": person.city,
            "postal_code": person.postal_code,
        }
    else:
        d = {
            "type": "natural_person",
            "first_name": person.first_name,
            "last_name": person.last_name,
            "birth_date": str(person.birth_date),
            "tax_id": person.tax_id,
            "street_address": person.street_address,
            "city": person.city,
            "postal_code": person.postal_code,
        }
        if getattr(person, "wid", None):
            d["wid"] = person.wid
        return d


def _add_person_xml(parent: ET.Element, tag: str, person) -> None:
    # Helper for XML formatting
    elem = ET.SubElement(parent, tag)
    if isinstance(person, LegalEntity):
        ET.SubElement(elem, "type").text = "legal_entity"
        ET.SubElement(elem, "name").text = person.name
        ET.SubElement(elem, "wid").text = person.wid
        ET.SubElement(elem, "street_address").text = person.street_address
        ET.SubElement(elem, "city").text = person.city
        ET.SubElement(elem, "postal_code").text = person.postal_code
    else:
        ET.SubElement(elem, "type").text = "natural_person"
        ET.SubElement(elem, "first_name").text = person.first_name
        ET.SubElement(elem, "last_name").text = person.last_name
        ET.SubElement(elem, "birth_date").text = str(person.birth_date)
        ET.SubElement(elem, "tax_id").text = person.tax_id
        if getattr(person, "wid", None):
            ET.SubElement(elem, "wid").text = person.wid
        ET.SubElement(elem, "street_address").text = person.street_address
        ET.SubElement(elem, "city").text = person.city
        ET.SubElement(elem, "postal_code").text = person.postal_code


def _output_results_stdout(
    ibans: List[IBANRecord],
    *,
    output_format: Optional[str],
    include_bank_info: bool,
    include_personal_info: bool,
) -> None:
    """Handle stdout output across supported formats (txt, csv, xml, json, or plain)."""
    formatter = OutputFormatter()
    if output_format is None:
        formatter.format_stdout(ibans, include_bank_info, include_personal_info)
        return

    if output_format == "txt":
        for record in ibans:
            holders_display = "; ".join(
                _format_person_inline(h) for h in record.account_holders
            )
            beneficiaries_strs = [
                _format_person_inline(b) for b in record.beneficiaries
            ]
            beneficiaries_display = (
                "; ".join(beneficiaries_strs) if beneficiaries_strs else "None"
            )

            if include_personal_info and include_bank_info:
                print(
                    f"{record.iban} | Holders: {holders_display} | Beneficiaries: {beneficiaries_display} | {record.bank.name} | {record.bank.bic} | {record.bank.bankleitzahl}"
                )
            elif include_personal_info:
                print(
                    f"{record.iban} | Holders: {holders_display} | Beneficiaries: {beneficiaries_display}"
                )
            elif include_bank_info:
                print(
                    f"{record.iban} | {record.bank.name} | {record.bank.bic} | {record.bank.bankleitzahl}"
                )
            else:
                print(record.iban)
    elif output_format == "csv":
        print("IBAN,Account Holders,Beneficial Owners,Bank Name,BIC,Bank Code")
        for record in ibans:
            holders_csv = "; ".join(
                _format_person_inline(h) for h in record.account_holders
            )
            beneficiaries_strs = [
                _format_person_inline(b) for b in record.beneficiaries
            ]
            beneficiaries_csv = (
                "; ".join(beneficiaries_strs) if beneficiaries_strs else "None"
            )

            print(
                f'"{record.iban}","{holders_csv}","{beneficiaries_csv}","{record.bank.name}","{record.bank.bic}","{record.bank.bankleitzahl}"'
            )
    elif output_format == "xml":
        import xml.dom.minidom

        root = ET.Element("accounts")
        for record in ibans:
            iban_elem = ET.SubElement(root, "account")
            ET.SubElement(iban_elem, "iban").text = record.iban

            holders_elem = ET.SubElement(iban_elem, "account_holders")
            for holder in record.account_holders:
                _add_person_xml(holders_elem, "holder", holder)

            beneficiaries_elem = ET.SubElement(iban_elem, "beneficiaries")
            for beneficiary in record.beneficiaries:
                _add_person_xml(beneficiaries_elem, "beneficiary", beneficiary)

            bank_elem = ET.SubElement(iban_elem, "bank")
            ET.SubElement(bank_elem, "name").text = record.bank.name
            ET.SubElement(bank_elem, "bic").text = record.bank.bic
            ET.SubElement(bank_elem, "code").text = record.bank.bankleitzahl
        rough_string = ET.tostring(root, "utf-8")
        reparsed = xml.dom.minidom.parseString(rough_string)
        print(reparsed.toprettyxml(indent="  ").split("\n", 1)[1])
    elif output_format == "json":
        data = []
        for record in ibans:
            holders_data = [_person_to_dict(h) for h in record.account_holders]
            beneficiaries_data = [_person_to_dict(b) for b in record.beneficiaries]
            data.append(
                {
                    "iban": record.iban,
                    "account_holders": holders_data,
                    "beneficiaries": beneficiaries_data,
                    "bank": {
                        "name": record.bank.name,
                        "bic": record.bank.bic,
                        "code": record.bank.bankleitzahl,
                    },
                }
            )
        print(json.dumps(data, ensure_ascii=False, indent=2))


if __name__ == "__main__":
    cli()
