"""Taegis Magic tenants commands."""

import inspect
import logging
from dataclasses import asdict, dataclass, field
from enum import Enum
from typing import List, Optional

import typer
from click.exceptions import BadOptionUsage
from dataclasses_json import dataclass_json
from taegis_magic.core.log import tracing
from taegis_magic.core.normalizer import TaegisResultsNormalizer
from taegis_magic.core.service import get_service
from taegis_magic.core.utils import remove_output_node
from typing_extensions import Annotated

from taegis_sdk_python import GraphQLNoRowsInResultSetError, build_output_string
from taegis_sdk_python.services.rules.types import (
    Rule,
    RuleEventType,
    RuleFilterInput,
    RuleInput,
    RuleQLFilterInput,
    RuleQueryKind,
    RuleType,
    SearchRulesInput,
    SearchRulesOutput,
)

log = logging.getLogger(__name__)

app = typer.Typer(help="Taegis Rules Commands.")
create_rule = typer.Typer(help="Create a Custom Alerting or Suppression Rules.")
app.add_typer(create_rule, name="create")


class RuleSeverity(str, Enum):
    """Rule Severity Options."""

    low = "low"
    medium = "medium"
    high = "high"
    critical = "critical"


RULE_SEVERTIY_MAP = {
    "low": 0.2,
    "medium": 0.4,
    "high": 0.6,
    "critical": 0.8,
}


class RuleSuppressionKey(str, Enum):
    """Suppression Rule keys."""

    alert_title = "alert_title"
    detector = "detector"
    entity = "entity"


RULE_SUPPRESSION_KEY_MAP = {
    "alert_title": "scwx.observation_v2.title",
    "detector": "scwx.observation_v2.detector_id",
    "entity": "scwx.observation_v2.entity",
}


def multiple_rules_output():
    return remove_output_node(
        build_output_string(Rule),
        "generativeAIRuleExplain",
    )


@dataclass_json
@dataclass
class TaegisRuleNormalizer(TaegisResultsNormalizer):
    """Taegis Single Rule normalizer."""

    raw_results: Rule = field(default_factory=lambda: Rule())

    @property
    def results(self):
        return [asdict(self.raw_results)]


@dataclass
class TaegisRulesNormalizer(TaegisResultsNormalizer):
    """Taegis Multiple Rule normalizer."""

    raw_results: List[Rule] = field(default_factory=list)

    @property
    def results(self):
        return [asdict(rule) for rule in self.raw_results]


@dataclass
class TaegisRulesSearchNormalizer(TaegisResultsNormalizer):
    """Taegis Search Rule normalizer."""

    raw_results: List[SearchRulesOutput] = field(
        default_factory=lambda: [SearchRulesOutput()]
    )

    @property
    def results(self):
        return [
            asdict(rule)
            for result in self.raw_results or []
            for rule in result.rules or []
        ]


@app.command(name="type")
@tracing
def rules_type(
    rule_type: Annotated[RuleType, typer.Option()] = RuleType.QL,
    tenant: Optional[str] = None,
    region: Optional[str] = None,
):
    """Taegis rules by type."""
    service = get_service(environment=region, tenant_id=tenant)

    page = 1
    per_page = 500
    rules = []

    log.info(f"Getting page: {page}")
    try:
        with service(output=multiple_rules_output()):
            results = service.rules.query.rules(
                page=page, count=per_page, rule_type=rule_type
            )
    except GraphQLNoRowsInResultSetError:  # pragma: no cover
        results = []
    log.debug(f"Results returned: {len(results)}")
    rules.extend(results)

    while len(results) == per_page:
        page += 1
        log.info(f"Getting page: {page}")
        try:
            with service(output=multiple_rules_output()):
                results = service.rules.query.rules(
                    page=page, count=per_page, rule_type=rule_type
                )
        except GraphQLNoRowsInResultSetError:  # pragma: no cover
            results = []
        log.debug(f"Results returned: {len(results)}")
        rules.extend(results)

    normalized_results = TaegisRulesNormalizer(
        raw_results=rules,
        service="rules",
        tenant_id=service.tenant_id,
        region=service.environment,
        arguments=inspect.currentframe().f_locals,
    )

    return normalized_results


@app.command(name="all")
@tracing
def rules_all(
    tenant: Optional[str] = None,
    region: Optional[str] = None,
):
    """Taegis all rules."""
    service = get_service(environment=region, tenant_id=tenant)

    page = 1
    per_page = 500
    rules = []

    log.info(f"Getting page: {page}")
    try:
        with service(output=multiple_rules_output()):
            results = service.rules.query.all_rules(page=page, count=per_page)
    except GraphQLNoRowsInResultSetError:  # pragma: no cover
        results = []
    log.debug(f"Results returned: {len(results)}")
    rules.extend(results)

    while len(results) == per_page:
        page += 1
        log.info(f"Getting page: {page}")
        try:
            with service(output=multiple_rules_output()):
                results = service.rules.query.all_rules(page=page, count=per_page)
        except GraphQLNoRowsInResultSetError:  # pragma: no cover
            results = []
        log.debug(f"Results returned: {len(results)}")
        rules.extend(results)

    normalized_results = TaegisRulesNormalizer(
        raw_results=rules,
        service="rules",
        tenant_id=service.tenant_id,
        region=service.environment,
        arguments=inspect.currentframe().f_locals,
    )

    return normalized_results


@app.command(name="suppression")
@tracing
def rules_suppression(
    kind: RuleQueryKind = RuleQueryKind.ALL,
    tenant: Optional[str] = None,
    region: Optional[str] = None,
):
    """Taegis suppression rules."""
    service = get_service(environment=region, tenant_id=tenant)

    page = 1
    per_page = 500
    rules = []

    log.info(f"Getting page: {page}")
    try:
        results = service.rules.query.suppression_rules(
            page=page,
            count=per_page,
            kind=kind,
        )
    except GraphQLNoRowsInResultSetError:  # pragma: no cover
        results = []
    log.debug(f"Results returned: {len(results)}")
    rules.extend(results)

    while len(results) == per_page:
        page += 1
        log.info(f"Getting page: {page}")
        try:
            results = service.rules.query.suppression_rules(
                page=page,
                count=per_page,
                kind=kind,
            )
        except GraphQLNoRowsInResultSetError:  # pragma: no cover
            results = []
        log.debug(f"Results returned: {len(results)}")
        rules.extend(results)

    normalized_results = TaegisRulesNormalizer(
        raw_results=rules,
        service="rules",
        tenant_id=service.tenant_id,
        region=service.environment,
        arguments=inspect.currentframe().f_locals,
    )

    return normalized_results


@app.command(name="deleted")
@tracing
def rules_deleted(
    rule_type: Annotated[RuleType, typer.Option()] = RuleType.QL,
    tenant: Optional[str] = None,
    region: Optional[str] = None,
):
    """Taegis deleted rules."""
    service = get_service(environment=region, tenant_id=tenant)

    page = 1
    per_page = 500
    rules = []

    log.info(f"Getting page: {page}")
    try:
        with service(output=multiple_rules_output()):
            results = service.rules.query.deleted_rules(
                page=page,
                count=per_page,
                rule_type=rule_type,
            )
    except GraphQLNoRowsInResultSetError:  # pragma: no cover
        results = []
    log.debug(f"Results returned: {len(results)}")
    rules.extend(results)

    while len(results) == per_page:
        page += 1
        log.info(f"Getting page: {page}")
        try:
            with service(output=multiple_rules_output()):
                results = service.rules.query.deleted_rules(
                    page=page,
                    count=per_page,
                    rule_type=rule_type,
                )
        except GraphQLNoRowsInResultSetError:  # pragma: no cover
            results = []
        log.debug(f"Results returned: {len(results)}")
        rules.extend(results)

    normalized_results = TaegisRulesNormalizer(
        raw_results=rules,
        service="rules",
        tenant_id=service.tenant_id,
        region=service.environment,
        arguments=inspect.currentframe().f_locals,
    )

    return normalized_results


@app.command(name="filters")
@tracing
def rules_event_type(
    event_type: Annotated[RuleEventType, typer.Option()],
    rule_type: Annotated[RuleType, typer.Option()] = RuleType.REGEX,
    tenant: Optional[str] = None,
    region: Optional[str] = None,
):
    """Taegis all rules."""
    service = get_service(environment=region, tenant_id=tenant)

    page = 1
    per_page = 500
    rules = []

    log.info(f"Getting page: {page}")
    try:
        with service(output=multiple_rules_output()):
            results = service.rules.query.rules_for_event(
                page=page, count=per_page, event_type=event_type, rule_type=rule_type
            )
    except GraphQLNoRowsInResultSetError:  # pragma: no cover
        results = []
    log.debug(f"Results returned: {len(results)}")
    rules.extend(results)

    while len(results) == per_page:
        page += 1
        log.info(f"Getting page: {page}")
        try:
            with service(output=multiple_rules_output()):
                results = service.rules.query.rules_for_event(
                    page=page,
                    count=per_page,
                    event_type=event_type,
                    rule_type=rule_type,
                )
        except GraphQLNoRowsInResultSetError:  # pragma: no cover
            results = []
        log.debug(f"Results returned: {len(results)}")
        rules.extend(results)

    normalized_results = TaegisRulesNormalizer(
        raw_results=rules,
        service="rules",
        tenant_id=service.tenant_id,
        region=service.environment,
        arguments=inspect.currentframe().f_locals,
    )

    return normalized_results


@app.command(name="rule")
@tracing
def rules_rule(
    rule_id: Annotated[str, typer.Option()],
    tenant: Optional[str] = None,
    region: Optional[str] = None,
):
    """Taegis rule by id."""
    service = get_service(environment=region, tenant_id=tenant)

    try:
        results = service.rules.query.rule(id_=rule_id)
    except GraphQLNoRowsInResultSetError:  # pragma: no cover
        results = Rule()

    normalized_results = TaegisRuleNormalizer(
        raw_results=results,
        service="rules",
        tenant_id=service.tenant_id,
        region=service.environment,
        arguments=inspect.currentframe().f_locals,
    )

    return normalized_results


@app.command("changes-since")
@tracing
def rules_changes_since(
    timestamp: Annotated[str, typer.Option(help="YYYY-DD-MMTHH:MM:SSZ")],
    event_type: Annotated[Optional[RuleEventType], typer.Option()] = None,
    rule_type: Annotated[RuleType, typer.Option()] = RuleType.REGEX,
    tenant: Optional[str] = None,
    region: Optional[str] = None,
):
    """Taegis rule changed by timestamp."""
    service = get_service(environment=region, tenant_id=tenant)

    try:
        results = service.rules.query.changes_since(
            timestamp=timestamp,
            event_type=event_type,
            rule_type=rule_type,
        )
    except GraphQLNoRowsInResultSetError:  # pragma: no cover
        results = []

    normalized_results = TaegisRulesNormalizer(
        raw_results=results,
        service="rules",
        tenant_id=service.tenant_id,
        region=service.environment,
        arguments=inspect.currentframe().f_locals,
    )

    return normalized_results


@app.command("search")
@tracing
def rules_search(
    cell: Annotated[str, typer.Option()],
    tenant: Optional[str] = None,
    region: Optional[str] = None,
):
    """Taegis rule changed by timestamp."""
    service = get_service(environment=region, tenant_id=tenant)

    page = 1
    per_page = 500
    rules = []

    log.info(f"Getting page: {page}")
    try:
        with service(
            output=remove_output_node(
                build_output_string(SearchRulesOutput), "generativeAIRuleExplain"
            )
        ):
            results = service.rules.query.search_rules(
                SearchRulesInput(query=cell),
                page=page,
                count=per_page,
            )
    except GraphQLNoRowsInResultSetError:  # pragma: no cover
        results = []
    log.debug(f"Results returned: {len(results.rules)}")
    rules.append(results)

    while len(results.rules) == per_page:
        page += 1
        log.info(f"Getting page: {page}")
        try:
            with service(
                output=remove_output_node(
                    build_output_string(SearchRulesOutput), "generativeAIRuleExplain"
                )
            ):
                results = service.rules.query.search_rules(
                    SearchRulesInput(query=cell),
                    page=page,
                    count=per_page,
                )
        except GraphQLNoRowsInResultSetError:  # pragma: no cover
            results = []
        log.debug(f"Results returned: {len(results.rules)}")
        rules.append(results)

    normalized_results = TaegisRulesSearchNormalizer(
        raw_results=rules,
        service="rules",
        tenant_id=service.tenant_id,
        region=service.environment,
        arguments=inspect.currentframe().f_locals,
    )

    return normalized_results


@app.command(name="enable")
@tracing
def rules_enable(
    rule_id: Annotated[str, typer.Option()],
    tenant: Optional[str] = None,
    region: Optional[str] = None,
):
    """Enable a Taegis rule."""

    service = get_service(environment=region, tenant_id=tenant)

    results = service.rules.mutation.enable_rule(id_=rule_id)

    normalized_results = TaegisRuleNormalizer(
        raw_results=results,
        service="rules",
        tenant_id=service.tenant_id,
        region=service.environment,
        arguments=inspect.currentframe().f_locals,
    )

    return normalized_results


@app.command(name="disable")
@tracing
def rules_disable(
    rule_id: Annotated[str, typer.Option()],
    tenant: Optional[str] = None,
    region: Optional[str] = None,
):
    """Disable a Taegis rule."""

    service = get_service(environment=region, tenant_id=tenant)

    results = service.rules.mutation.disable_rule(id_=rule_id)

    normalized_results = TaegisRuleNormalizer(
        raw_results=results,
        service="rules",
        tenant_id=service.tenant_id,
        region=service.environment,
        arguments=inspect.currentframe().f_locals,
    )

    return normalized_results


@create_rule.command(name="custom")
@tracing
def rules_create_custom_rule(
    cell: Annotated[str, typer.Option()],
    name: Annotated[str, typer.Option()],
    description: Annotated[str, typer.Option()],
    severity: Annotated[RuleSeverity, typer.Option()],
    mitre_category: Annotated[Optional[List[str]], typer.Option()] = None,
    tenant: Optional[str] = None,
    region: Optional[str] = None,
):
    """Taegis Create Custom QL Rule."""
    service = get_service(environment=region, tenant_id=tenant)

    results = service.rules.mutation.create_custom_ql_rule(
        input_=RuleInput(
            name=name,
            description=description,
            severity=RULE_SEVERTIY_MAP[severity.value],
            create_alert=True,
            attack_categories=mitre_category,
        ),
        ql_filter=RuleQLFilterInput(query=cell),
    )

    normalized_results = TaegisRuleNormalizer(
        raw_results=results,
        service="rules",
        tenant_id=service.tenant_id,
        region=service.environment,
        arguments=inspect.currentframe().f_locals,
    )

    return normalized_results


@create_rule.command(name="suppression")
@tracing
def rules_create_suppression_rule(
    name: Annotated[str, typer.Option()],
    description: Annotated[str, typer.Option()],
    key: Annotated[List[RuleSuppressionKey], typer.Option()],
    pattern: Annotated[
        List[str],
        typer.Option(help="Regex pattern match, ensure you escape literals (ie '\\.')"),
    ],
    inverted: Annotated[
        Optional[List[str]],
        typer.Option(
            help="required for all if a single filter is inverted, set to True/true, all other values will be False"
        ),
    ] = None,
    tenant: Optional[str] = None,
    region: Optional[str] = None,
):
    """Taegis Create Suppression Rule."""
    service = get_service(environment=region, tenant_id=tenant)

    if len(key) != len(pattern):  # pragma: no cover
        raise BadOptionUsage(..., "key and pattern lengths must match")

    if inverted and len(key) != len(inverted):  # pragma: no cover
        raise BadOptionUsage(..., "key and inverted lengths must match")

    if inverted:
        inverted = [True if i.lower() == "true" else False for i in inverted]
    else:
        inverted = [False for _ in key]

    entity_prefixes = service.rules.query.entity_prefixes()
    filters = []

    for k, p, i in zip(key, pattern, inverted):
        if k == RuleSuppressionKey.entity:
            if ":" not in p:  # pragma: no cover
                raise BadOptionUsage(
                    ...,
                    f"'entity' patterns must be formatted 'prefix:pattern', prefix must be in value: {list(entity_prefixes.keys())}",
                )

            entity_prefix = p.split(":", maxsplit=1)[0]

            if entity_prefix not in entity_prefixes:  # pragma: no cover
                raise BadOptionUsage(
                    ...,
                    f"'entity' patterns must be formatted 'prefix:pattern', prefix must be in value: {list(entity_prefixes.keys())}",
                )

        filters.append(
            RuleFilterInput(
                key=RULE_SUPPRESSION_KEY_MAP[k.value], pattern=p, inverted=i
            )
        )

    results = service.rules.mutation.create_custom_suppression_rule(
        input_=RuleInput(
            name=name,
            description=description,
        ),
        filters=filters,
    )

    normalized_results = TaegisRuleNormalizer(
        raw_results=results,
        service="rules",
        tenant_id=service.tenant_id,
        region=service.environment,
        arguments=inspect.currentframe().f_locals,
    )

    return normalized_results
