"""Common utilities: config loading, NETCONF connection, target resolution, parallel execution."""

from concurrent import futures
from jnpr.junos import Device
from jnpr.junos.exception import (
    ConnectAuthError,
    ConnectClosedError,
    ConnectError,
    ConnectRefusedError,
    ConnectTimeoutError,
    ConnectUnknownHostError,
)
import configparser
import os
import sys
import threading
from logging import getLogger

logger = getLogger(__name__)

config = None
config_lock = threading.Lock()
args = None

DEFAULT_CONFIG = "config.ini"


def get_default_config():
    """Search for config file in standard locations."""
    # カレントディレクトリ
    if os.path.isfile(DEFAULT_CONFIG):
        return DEFAULT_CONFIG
    # XDG_CONFIG_HOME（未設定なら ~/.config）
    xdg = os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config"))
    xdg_path = os.path.join(xdg, "junos-ops", DEFAULT_CONFIG)
    if os.path.isfile(xdg_path):
        return xdg_path
    return DEFAULT_CONFIG


def read_config():
    """Read and parse the INI config file."""
    global config
    config = configparser.ConfigParser(allow_no_value=True)
    config.read(args.config)
    if len(config.sections()) == 0:
        print(args.config, "is empty")
        return True
    for section in config.sections():
        if config.has_option(section, "host"):
            host = config.get(section, "host")
        else:
            host = None
        if host is None:
            # host is [section] name
            config.set(section, "host", section)
        if args.debug:
            for key in config[section]:
                print(section, ">", key, ":", config[section][key])
            print()
    return False


def connect(hostname):
    """Open NETCONF connection to a device."""
    if args.debug:
        print("connect: start")
    dev = Device(
        host=config.get(hostname, "host"),
        port=int(config.get(hostname, "port")),
        user=config.get(hostname, "id"),
        passwd=config.get(hostname, "pw"),
        ssh_private_key_file=os.path.expanduser(config.get(hostname, "sshkey")),
        huge_tree=config.getboolean(hostname, "huge_tree", fallback=False),
    )
    err = None
    try:
        dev.open()
        err = False
    except ConnectAuthError as e:
        print("Authentication credentials fail to login: {0}".format(e))
        dev = None
        err = True
    except ConnectRefusedError as e:
        print("NETCONF Connection refused: {0}".format(e))
        dev = None
        err = True
    except ConnectTimeoutError as e:
        print("Connection timeout: {0}".format(e))
        dev = None
        err = True
    except ConnectError as e:
        print("Cannot connect to device: {0}".format(e))
        dev = None
        err = True
    except ConnectUnknownHostError as e:
        print("Unknown Host: {0}".format(e))
        dev = None
        err = True
    except Exception as e:
        print(e)
        dev = None
        err = True
    if args.debug:
        print("connect: err=", err, "dev=", dev)
    if args.debug:
        print("connect: end")
    return err, dev


def _get_host_tags(section: str) -> set[str]:
    """Return the set of tags for a config section."""
    raw = config.get(section, "tags", fallback="")
    if not raw.strip():
        return set()
    return {t.strip().lower() for t in raw.split(",")}


def _filter_by_tags(required_tags: set[str]) -> list[str]:
    """Return sections whose tags are a superset of required_tags (AND)."""
    matched = []
    for section in config.sections():
        if required_tags <= _get_host_tags(section):
            matched.append(section)
    return matched


def get_targets():
    """Return target host list from CLI args, tags, or config sections."""
    tags = getattr(args, "tags", None)
    has_hosts = len(args.specialhosts) > 0

    # タグ指定時: パースして AND フィルタ用の set を作成
    if tags is not None:
        required_tags = {t.strip().lower() for t in tags.split(",")}
    else:
        required_tags = set()

    # パターン1: --tags なし & hosts なし → 全セクション（現行動作）
    if not required_tags and not has_hosts:
        targets = []
        for i in config.sections():
            tmp = config.get(i, "host")
            logger.debug(f"{i=} {tmp=}")
            if tmp is not None:
                targets.append(i)
            else:
                print(i, "is not found in", args.config)
                sys.exit(1)
        return targets

    # パターン2: --tags なし & hosts あり → 指定ホストのみ（現行動作）
    if not required_tags and has_hosts:
        targets = []
        for i in args.specialhosts:
            if config.has_section(i):
                tmp = config.get(i, "host")
            else:
                print(i, "is not found in", args.config)
                sys.exit(1)
            logger.debug(f"{i=} {tmp=}")
            targets.append(i)
        return targets

    # パターン3: --tags あり & hosts なし → タグで AND フィルタ
    if required_tags and not has_hosts:
        targets = _filter_by_tags(required_tags)
        if not targets:
            print("no hosts matched tags:", tags)
            sys.exit(1)
        return targets

    # パターン4: --tags あり & hosts あり → タグフィルタ結果 ∪ hosts（重複排除）
    tag_matched = _filter_by_tags(required_tags)
    seen = set()
    targets = []
    # タグマッチ分を先に追加
    for i in tag_matched:
        if i not in seen:
            seen.add(i)
            targets.append(i)
    # 明示指定ホストを追加（存在チェック付き）
    for i in args.specialhosts:
        if not config.has_section(i):
            print(i, "is not found in", args.config)
            sys.exit(1)
        if i not in seen:
            seen.add(i)
            targets.append(i)
    return targets


def load_commands(filepath: str) -> list[str]:
    """Load command lines from a file, stripping blank lines and comments.

    Lines starting with '#' are treated as comments and excluded.
    """
    with open(filepath) as f:
        return [
            line.strip() for line in f
            if line.strip() and not line.strip().startswith("#")
        ]


def run_parallel(func, targets, max_workers=1):
    """Run a function against targets using ThreadPoolExecutor.

    When max_workers=1, runs serially for backward compatibility.
    """
    if max_workers <= 1:
        results = {}
        for target in targets:
            results[target] = func(target)
        return results

    with futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
        future_to_target = {
            executor.submit(func, target): target
            for target in targets
        }
        results = {}
        for future in futures.as_completed(future_to_target):
            target = future_to_target[future]
            try:
                results[target] = future.result()
            except Exception as e:
                logger.error(f"{target} generated an exception: {e}")
                results[target] = 1
        return results
