import asyncio
import json
import logging
import os
import time
from typing import Dict, List, Tuple, Optional

from tabulate import tabulate
from rich.console import Console
from rich.table import Table

from dracs.exceptions import (
    DatabaseError,
    DracsError,
    SNMPError,
    ValidationError,
)
from dracs.db import (
    System,
    get_session,
    db_initialize,
    query_by_service_tag,
    query_by_hostname,
    query_by_model,
    query_all_systems,
    upsert_system,
)
from dracs.snmp import get_snmp_value, build_idrac_hostname
from dracs.api import dell_api_warranty_date

logger = logging.getLogger(__name__)

debug_output = False


async def add_dell_warranty(
    service_tag: str,
    hostname: str,
    model: str,
    warranty: str,
    warranty_results: Optional[Dict] = None,
) -> None:
    idrac_host = build_idrac_hostname(hostname)
    community_string = os.getenv("SNMP_COMMUNITY", "public")
    BIOS_OID = "1.3.6.1.4.1.674.10892.5.4.300.50.1.8.1.1"
    IDRAC_FW_OID = "1.3.6.1.4.1.674.10892.5.1.1.8.0"

    bios_version = await get_snmp_value(idrac_host, community_string, BIOS_OID)
    idrac_version = await get_snmp_value(idrac_host, community_string, IDRAC_FW_OID)

    logger.info(f"Retrieved SNMP values - BIOS: {bios_version}, iDRAC: {idrac_version}")

    db_initialize(warranty)

    with get_session() as session:
        results = (
            session.query(System)
            .filter(System.svc_tag == service_tag, System.name == hostname)
            .all()
        )

    if debug_output:
        logger.debug(f"service_tag = {service_tag}")
        logger.debug(f"hostname = {hostname}")
        logger.debug(f"warranty = {warranty}")
        logger.debug(f"results = {results}")

    if len(results) > 1:
        raise DatabaseError("Multiple matching records found in database")

    if len(results) == 1:
        logger.info(f"Updating existing record for {service_tag}")
        upsert_system(
            warranty,
            service_tag,
            hostname,
            model,
            idrac_version,
            bios_version,
            results[0].exp_date,
            results[0].exp_epoch,
        )
        logger.info(f"Successfully updated record for {service_tag}")
    else:
        logger.info(
            f"Adding new record for {service_tag}, fetching warranty from Dell API"
        )
        if warranty_results is None:
            warranty_results = dell_api_warranty_date(service_tag)
        h_epoch, h_date = warranty_results[service_tag]

        if debug_output:
            logger.debug(
                f"Warranty result: svc_tag={service_tag}, exp_date={h_date}, "
                f"exp_epoch={h_epoch}"
            )

        upsert_system(
            warranty,
            service_tag,
            hostname,
            model,
            idrac_version,
            bios_version,
            h_date,
            h_epoch,
        )
        logger.info(f"Successfully added record for {service_tag}")


async def edit_dell_warranty(
    service_tag: Optional[str],
    hostname: Optional[str],
    model: Optional[str],
    idrac: bool,
    bios: bool,
    warranty: str,
) -> None:
    if service_tag:
        if debug_output:
            print(f"service_tag = {service_tag}")
    if hostname:
        if debug_output:
            print(f"hostname = {hostname}")
    if model:
        if debug_output:
            logger.debug(f"model = {model}")
    else:
        if not idrac and not bios:
            raise ValidationError(
                "Model parameter required for edit mode when not updating idrac or bios"
            )

    db_initialize(warranty)

    with get_session() as session:
        if service_tag:
            results = session.query(System).filter(System.svc_tag == service_tag).all()
        elif hostname:
            results = session.query(System).filter(System.name == hostname).all()
        else:
            results = []

    if debug_output:
        logger.debug(f"service_tag = {service_tag}")
        logger.debug(f"hostname = {hostname}")
        logger.debug(f"warranty = {warranty}")
        logger.debug(f"results = {results}")

    if len(results) > 1:
        raise DatabaseError("Multiple matching records found in database")

    if len(results) == 1:
        record = results[0]
        hostname = record.name
        idrac_host = build_idrac_hostname(hostname)
        community_string = os.getenv("SNMP_COMMUNITY", "public")
        BIOS_OID = "1.3.6.1.4.1.674.10892.5.4.300.50.1.8.1.1"
        IDRAC_FW_OID = "1.3.6.1.4.1.674.10892.5.1.1.8.0"

        if idrac:
            idrac_version = await get_snmp_value(
                idrac_host, community_string, IDRAC_FW_OID
            )
        else:
            idrac_version = record.idrac_version
        if bios:
            bios_version = await get_snmp_value(idrac_host, community_string, BIOS_OID)
        else:
            bios_version = record.bios_version
        if not model:
            model = record.model

        upsert_system(
            warranty,
            record.svc_tag,
            record.name,
            model,
            idrac_version,
            bios_version,
            record.exp_date,
            record.exp_epoch,
        )
        if debug_output:
            logger.info("Database updated successfully")
    else:
        raise DatabaseError("Record not found in database")


async def lookup_dell_warranty(
    service_tag: Optional[str],
    hostname: Optional[str],
    idrac: bool,
    bios: bool,
    full: bool,
    warranty: str,
) -> None:
    db_initialize(warranty)

    with get_session() as session:
        if service_tag:
            results = session.query(System).filter(System.svc_tag == service_tag).all()
        elif hostname:
            results = session.query(System).filter(System.name == hostname).all()
        else:
            results = []

    if len(results) == 0:
        raise DatabaseError("No matching records found in database")
    if len(results) > 1:
        raise DatabaseError("Multiple matching records found in database")

    record = results[0]
    result = {"hostname": record.name}
    if idrac or full:
        result["idrac_version"] = record.idrac_version
    if bios or full:
        result["bios_version"] = record.bios_version
    result["svc_tag"] = record.svc_tag
    if not idrac and not bios:
        result["model"] = record.model
        result["exp_date"] = record.exp_date
        result["exp_epoch"] = record.exp_epoch
    print(result)


async def filter_list_results(
    results: List[Tuple],
    bios_le: Optional[str],
    bios_lt: Optional[str],
    bios_ge: Optional[str],
    bios_gt: Optional[str],
    bios_eq: Optional[str],
    idrac_le: Optional[str],
    idrac_lt: Optional[str],
    idrac_ge: Optional[str],
    idrac_gt: Optional[str],
    idrac_eq: Optional[str],
) -> List[Tuple]:
    output = []
    for s in results:
        s_idrac = s[3]
        s_idrac_tuple = tuple(map(int, s_idrac.split(".")))
        s_bios = s[4]
        s_bios_tuple = tuple(map(int, s_bios.split(".")))
        if idrac_le:
            idrac_le_tuple = tuple(map(int, idrac_le.split(".")))
            if s_idrac_tuple <= idrac_le_tuple:
                output.append(s)
        if idrac_lt:
            idrac_lt_tuple = tuple(map(int, idrac_lt.split(".")))
            if s_idrac_tuple < idrac_lt_tuple:
                output.append(s)
        if idrac_ge:
            idrac_ge_tuple = tuple(map(int, idrac_ge.split(".")))
            if s_idrac_tuple >= idrac_ge_tuple:
                output.append(s)
        if idrac_gt:
            idrac_gt_tuple = tuple(map(int, idrac_gt.split(".")))
            if s_idrac_tuple > idrac_gt_tuple:
                output.append(s)
        if idrac_eq:
            idrac_eq_tuple = tuple(map(int, idrac_eq.split(".")))
            if s_idrac_tuple == idrac_eq_tuple:
                output.append(s)
        if bios_le:
            bios_le_tuple = tuple(map(int, bios_le.split(".")))
            if s_bios_tuple <= bios_le_tuple:
                output.append(s)
        if bios_lt:
            bios_lt_tuple = tuple(map(int, bios_lt.split(".")))
            if s_bios_tuple < bios_lt_tuple:
                output.append(s)
        if bios_ge:
            bios_ge_tuple = tuple(map(int, bios_ge.split(".")))
            if s_bios_tuple >= bios_ge_tuple:
                output.append(s)
        if bios_gt:
            bios_gt_tuple = tuple(map(int, bios_gt.split(".")))
            if s_bios_tuple > bios_gt_tuple:
                output.append(s)
        if bios_eq:
            bios_eq_tuple = tuple(map(int, bios_eq.split(".")))
            if s_bios_tuple == bios_eq_tuple:
                output.append(s)

    return output


async def list_dell_warranty(
    service_tag: Optional[str],
    hostname: Optional[str],
    model: Optional[str],
    regex: Optional[str],
    bios_le: Optional[str],
    bios_lt: Optional[str],
    bios_ge: Optional[str],
    bios_gt: Optional[str],
    bios_eq: Optional[str],
    idrac_le: Optional[str],
    idrac_lt: Optional[str],
    idrac_ge: Optional[str],
    idrac_gt: Optional[str],
    idrac_eq: Optional[str],
    expires_in: Optional[str],
    expired: bool,
    printjson: bool,
    host_only: bool,
    warranty: str,
) -> None:
    db_initialize(warranty)

    if service_tag and hostname:
        raise ValidationError(
            "Cannot specify both --svctag and --target; they are mutually exclusive"
        )
    if (hostname or service_tag) and (model or regex):
        raise ValidationError(
            "Cannot specify --model or --regex when using --svctag or --target"
        )

    with get_session() as session:
        query = session.query(System)

        if service_tag:
            query = query.filter(System.svc_tag == service_tag)
        elif hostname:
            query = query.filter(System.name == hostname)
        else:
            if model and regex:
                query = query.filter(System.name.like(regex), System.model == model)
            elif model:
                query = query.filter(System.model == model)
            elif regex:
                query = query.filter(System.name.like(regex))

        if expires_in:
            current_time = int(time.time())
            future_timestamp = current_time + (int(expires_in) * 86400)
            query = query.filter(
                System.exp_epoch > current_time,
                System.exp_epoch <= future_timestamp,
            )

        if expired:
            current_time = int(time.time())
            query = query.filter(System.exp_epoch < current_time)

        query = query.order_by(System.name)
        records = query.all()
        results = [r.to_tuple() for r in records]

    if (
        bios_le
        or bios_lt
        or bios_ge
        or bios_gt
        or bios_eq
        or idrac_le
        or idrac_lt
        or idrac_ge
        or idrac_gt
        or idrac_eq
    ):
        results = await filter_list_results(
            results,
            bios_le,
            bios_lt,
            bios_ge,
            bios_gt,
            bios_eq,
            idrac_le,
            idrac_lt,
            idrac_ge,
            idrac_gt,
            idrac_eq,
        )
    if host_only:
        for row in results:
            print(row[1])
    elif printjson:
        print(json.dumps(results, indent=4))
    else:
        firmware_by_model = {}
        bios_by_model = {}

        for row in results:
            model = row[2]
            firmware = row[3]
            bios = row[4]

            if model and firmware:
                if model not in firmware_by_model:
                    firmware_by_model[model] = set()
                firmware_by_model[model].add(firmware)

            if model and bios:
                if model not in bios_by_model:
                    bios_by_model[model] = set()
                bios_by_model[model].add(bios)

        for model in firmware_by_model:
            firmware_by_model[model] = sorted(
                firmware_by_model[model],
                key=lambda v: tuple(map(int, v.split("."))),
                reverse=True,
            )

        for model in bios_by_model:
            bios_by_model[model] = sorted(
                bios_by_model[model],
                key=lambda v: tuple(map(int, v.split("."))),
                reverse=True,
            )

        console = Console()
        table = Table(show_header=True, header_style="bold cyan")
        table.add_column("Service Tag")
        table.add_column("Hostname")
        table.add_column("Model")
        table.add_column("Firmware")
        table.add_column("BIOS")
        table.add_column("Expires")

        current_time = int(time.time())
        ninety_days_future = current_time + (90 * 86400)

        for row in results:
            model = str(row[2])
            firmware = str(row[3])
            bios = str(row[4])
            exp_epoch = int(row[6])
            exp_date = str(row[5])

            if exp_epoch < current_time:
                colored_exp_date = f"[red]{exp_date}[/red]"
            elif exp_epoch <= ninety_days_future:
                colored_exp_date = f"[yellow]{exp_date}[/yellow]"
            else:
                colored_exp_date = exp_date

            if model in firmware_by_model and firmware in firmware_by_model[model]:
                firmware_index = firmware_by_model[model].index(firmware)
                if firmware_index == 1:
                    colored_firmware = f"[yellow]{firmware}[/yellow]"
                elif firmware_index >= 2:
                    colored_firmware = f"[red]{firmware}[/red]"
                else:
                    colored_firmware = firmware
            else:
                colored_firmware = firmware

            if model in bios_by_model and bios in bios_by_model[model]:
                bios_index = bios_by_model[model].index(bios)
                if bios_index == 1:
                    colored_bios = f"[yellow]{bios}[/yellow]"
                elif bios_index >= 2:
                    colored_bios = f"[red]{bios}[/red]"
                else:
                    colored_bios = bios
            else:
                colored_bios = bios

            table.add_row(
                str(row[0]),
                str(row[1]),
                model,
                colored_firmware,
                colored_bios,
                colored_exp_date,
            )

        console.print(table)


async def refresh_dell_warranty(
    service_tag: Optional[str],
    hostname: Optional[str],
    warranty: str,
    verbose: bool = False,
) -> None:
    db_initialize(warranty)

    if service_tag:
        results = query_by_service_tag(warranty, service_tag)
    elif hostname:
        results = query_by_hostname(warranty, hostname)
    else:
        raise ValidationError("Either service tag or hostname must be provided")

    if len(results) == 0:
        raise DatabaseError("No matching record found to refresh")
    if len(results) > 1:
        raise DatabaseError("Multiple matching records found in database")

    existing = results[0]
    svc_tag = existing[0]
    name = existing[1]
    old_model = existing[2]
    old_idrac_version = existing[3]
    old_bios_version = existing[4]
    old_exp_date = existing[5]

    if verbose:
        print(f"Refreshing {name} ... ", end="", flush=True)

    logger.debug(f"Refreshing data for {svc_tag} ({name})")

    idrac_host = build_idrac_hostname(name)
    community_string = os.getenv("SNMP_COMMUNITY", "public")
    BIOS_OID = "1.3.6.1.4.1.674.10892.5.4.300.50.1.8.1.1"
    IDRAC_FW_OID = "1.3.6.1.4.1.674.10892.5.1.1.8.0"
    MODEL_OID = ".1.3.6.1.4.1.674.10892.5.1.3.12.0"

    bios_version = await get_snmp_value(idrac_host, community_string, BIOS_OID)
    idrac_version = await get_snmp_value(idrac_host, community_string, IDRAC_FW_OID)
    model = await get_snmp_value(idrac_host, community_string, MODEL_OID)

    if model and model.startswith("PowerEdge "):
        model = model.replace("PowerEdge ", "")

    logger.debug(
        f"Updated SNMP values - Model: {model}, BIOS: {bios_version}, iDRAC: {idrac_version}"
    )

    warranty_results = dell_api_warranty_date(svc_tag)
    exp_epoch, exp_date = warranty_results[svc_tag]

    logger.debug(f"Updated warranty expiration: {exp_date}")

    upsert_system(
        warranty, svc_tag, name, model, idrac_version, bios_version, exp_date, exp_epoch
    )

    logger.debug(f"Successfully refreshed record for {svc_tag}")

    if verbose:
        print("done.")

    # Check for changes and report them
    from datetime import datetime

    timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")

    if old_model != model:
        print(
            f"{timestamp} - dracs.cli - INFO - {name} updated: Model changed from {old_model} to {model}"
        )

    if old_idrac_version != idrac_version:
        print(
            f"{timestamp} - dracs.cli - INFO - {name} updated: Firmware changed from {old_idrac_version} to {idrac_version}"
        )

    if old_bios_version != bios_version:
        print(
            f"{timestamp} - dracs.cli - INFO - {name} updated: BIOS changed from {old_bios_version} to {bios_version}"
        )

    if old_exp_date != exp_date:
        print(
            f"{timestamp} - dracs.cli - INFO - {name} updated: Warranty Expiration changed from {old_exp_date} to {exp_date}"
        )


async def refresh_by_model(model: str, warranty: str, verbose: bool = False) -> None:
    db_initialize(warranty)
    results = query_by_model(warranty, model)

    if len(results) == 0:
        raise DatabaseError(f"No systems found with model {model}")

    success_count = 0
    for idx, result in enumerate(results, start=1):
        hostname = result[1]
        if verbose:
            print(f"Refreshing {hostname} ... ", end="", flush=True)
        try:
            await refresh_dell_warranty(None, hostname, warranty, verbose=False)
            if verbose:
                print("done.")
            success_count += 1
        except Exception as e:
            if verbose:
                print(f"failed: {e}")
            logger.error(f"Failed to refresh {hostname}: {e}")

        # Throttle after every 100 systems to avoid overwhelming Dell API
        if idx % 100 == 0 and idx < len(results):
            print("[ Waiting 30 seconds before proceeding ]")
            await asyncio.sleep(30)

    print(f"Summary: A total of {success_count} hosts refreshed")


async def refresh_all_systems(warranty: str, verbose: bool = False) -> None:
    db_initialize(warranty)
    results = query_all_systems(warranty)

    if len(results) == 0:
        raise DatabaseError("No systems found in database")

    success_count = 0
    for idx, result in enumerate(results, start=1):
        hostname = result[1]
        if verbose:
            print(f"Refreshing {hostname} ... ", end="", flush=True)
        try:
            await refresh_dell_warranty(None, hostname, warranty, verbose=False)
            if verbose:
                print("done.")
            success_count += 1
        except Exception as e:
            if verbose:
                print(f"failed: {e}")
            logger.error(f"Failed to refresh {hostname}: {e}")

        # Throttle after every 100 systems to avoid overwhelming Dell API
        if idx % 100 == 0 and idx < len(results):
            print("[ Waiting 30 seconds before proceeding ]")
            await asyncio.sleep(30)

    print(f"Summary: A total of {success_count} hosts refreshed")


async def discover_dell_system(hostname: str, warranty: str) -> Tuple[str, str]:
    logger.info(f"Discovering system information for {hostname}")

    idrac_host = build_idrac_hostname(hostname)
    community_string = os.getenv("SNMP_COMMUNITY", "public")

    SERVICE_TAG_OID = ".1.3.6.1.4.1.674.10892.5.1.3.2.0"
    MODEL_OID = ".1.3.6.1.4.1.674.10892.5.1.3.12.0"

    logger.info(f"Querying {idrac_host} for service tag and model")

    service_tag = await get_snmp_value(idrac_host, community_string, SERVICE_TAG_OID)
    if not service_tag:
        raise SNMPError(f"Failed to retrieve service tag from {idrac_host}")

    model = await get_snmp_value(idrac_host, community_string, MODEL_OID)
    if not model:
        raise SNMPError(f"Failed to retrieve model from {idrac_host}")

    if model.startswith("PowerEdge "):
        model = model.replace("PowerEdge ", "")

    logger.info(f"Discovered: Service Tag={service_tag}, Model={model}")

    return (service_tag, model)


async def _discover_single_host(hostname: str, warranty: str, auto_add: bool) -> dict:
    result = {"hostname": hostname, "status": "ok", "error": None}
    try:
        service_tag, model = await discover_dell_system(hostname, warranty)
        result["service_tag"] = service_tag
        result["model"] = model

        if auto_add:
            await add_dell_warranty(service_tag, hostname, model, warranty)
            result["added"] = True
        else:
            result["added"] = False
    except (SNMPError, DracsError) as e:
        result["status"] = "error"
        result["error"] = str(e)
        logger.error(f"Failed to discover {hostname}: {e}")

    return result


async def discover_dell_systems_batch(
    hosts: List[str], warranty: str, auto_add: bool, show_discovered: bool = False
) -> None:
    tasks = [_discover_single_host(h, warranty, False) for h in hosts]
    results = await asyncio.gather(*tasks)

    if auto_add:
        discovered = [r for r in results if r["status"] == "ok"]
        if discovered:
            tags = [r["service_tag"] for r in discovered]
            warranty_results = dell_api_warranty_date(tags)

            add_tasks = [
                add_dell_warranty(
                    r["service_tag"],
                    r["hostname"],
                    r["model"],
                    warranty,
                    warranty_results=warranty_results,
                )
                for r in discovered
            ]
            add_outcomes = await asyncio.gather(*add_tasks, return_exceptions=True)

            for r, outcome in zip(discovered, add_outcomes):
                if isinstance(outcome, Exception):
                    r["status"] = "error"
                    r["error"] = str(outcome)
                else:
                    r["added"] = True

    succeeded = [r for r in results if r["status"] == "ok"]
    failed = [r for r in results if r["status"] == "error"]

    if succeeded and show_discovered:
        table_data = [
            (
                r["hostname"],
                r["service_tag"],
                r["model"],
                "Added" if r.get("added") else "Discovered",
            )
            for r in succeeded
        ]
        headers = ["Hostname", "Service Tag", "Model", "Status"]
        print(tabulate(table_data, headers=headers, tablefmt="grid"))

    if succeeded:
        print(f"\nSucceeded: {len(succeeded)}")

    if failed:
        print(f"Failed: {len(failed)}")
        table_data = [(r["hostname"], r["error"]) for r in failed]
        headers = ["Hostname", "Error"]
        print(tabulate(table_data, headers=headers, tablefmt="grid"))

    total = len(results)
    print(f"Total: {total} hosts")


async def remove_dell_warranty(
    service_tag: Optional[str], hostname: Optional[str], warranty: str
) -> None:
    if service_tag:
        if debug_output:
            print(f"service_tag = {service_tag}")
    if hostname:
        if debug_output:
            print(f"hostname = {hostname}")

    db_initialize(warranty)

    with get_session() as session:
        if service_tag:
            results = session.query(System).filter(System.svc_tag == service_tag).all()
        elif hostname:
            results = session.query(System).filter(System.name == hostname).all()
        else:
            results = []

        if len(results) == 0:
            raise DatabaseError("No matching records found in database")
        if len(results) > 1:
            raise DatabaseError("Multiple matching records found in database")

        record = results[0]
        session.delete(record)
        session.commit()
        print("Record deleted")
