# Copyright (c) CloudZero - ALL RIGHTS RESERVED - PROPRIETARY AND CONFIDENTIAL
# Unauthorized copying of this file and/or project, via any medium is strictly prohibited.
# Direct all questions to legal@cloudzero.com

"""CostFormation mapping conversion utilities.

Converts CSV mapping files to CloudZero CostFormation YAML format.
"""

from __future__ import annotations

import csv
import os
import re
from dataclasses import dataclass, field

import yaml
from faker import Faker
from rich.console import Console
from rich.panel import Panel
from rich.table import Table
from rich.text import Text

# Reserved column names for date range support
RESERVED_DATE_COLUMNS = {"From", "Until"}

# Date format pattern for validation
DATE_PATTERN = re.compile(r"^\d{4}-\d{2}-\d{2}$")


@dataclass
class ConversionConfig:
    """Configuration for CSV to CostFormation conversion."""

    input_file: str
    output_file: str | None
    use_contains: bool
    normalize: bool


@dataclass
class MappingContext:
    """State maintained during CSV to CostFormation conversion."""

    costformation: dict
    group_rules: dict[str, dict[tuple, dict]] = field(default_factory=dict)
    normalized_names: dict[str, str] = field(default_factory=dict)
    source_dimension: str = ""
    dimension_names: list[str] = field(default_factory=list)
    rows_processed: int = 0


def parse_arguments(arguments: dict) -> ConversionConfig:
    """Parse CLI arguments into ConversionConfig."""
    input_file = arguments.get("--file")
    if not input_file:
        raise ValueError("--file parameter is required")

    output_file = arguments.get("-o") or arguments.get("--output")

    return ConversionConfig(
        input_file=input_file,
        output_file=output_file,
        use_contains=bool(arguments.get("--condition-contains")),
        normalize=bool(arguments.get("--normalize")),
    )


def resolve_output_path(input_file: str, output_arg: str | None) -> str:
    """Determine the output file path from input file and optional output argument."""
    if output_arg:
        base_path = output_arg
    else:
        base_name = os.path.basename(input_file)
        name_without_ext = os.path.splitext(base_name)[0]
        output_dir = os.path.dirname(input_file)
        base_path = os.path.join(output_dir, name_without_ext)

    return f"{base_path}_rules.cz.yaml"


def validate_date_format(date_str: str) -> bool:
    """Validate that a date string matches YYYY-MM-DD format."""
    return bool(DATE_PATTERN.match(date_str))


def get_date_range_condition(from_date: str, until_date: str) -> dict | None:
    """Create a ForDateRange condition if both dates are provided and valid.

    Args:
        from_date: Start date in YYYY-MM-DD format
        until_date: End date in YYYY-MM-DD format

    Returns:
        ForDateRange condition dict, or None if dates are missing/invalid
    """
    from_date = from_date.strip() if from_date else ""
    until_date = until_date.strip() if until_date else ""

    if not from_date or not until_date:
        return None

    if not validate_date_format(from_date) or not validate_date_format(until_date):
        return None

    return {"ForDateRange": {"From": from_date, "Until": until_date}}


def write_yaml(costformation: dict, output_path: str) -> None:
    """Write CostFormation structure to YAML file."""
    with open(output_path, "w") as outfile:
        yaml.dump(costformation, outfile, default_flow_style=False, sort_keys=False)


def get_account_id():
    faker = Faker()
    return faker.random_int(10000000000, 99999999999)


def get_business_unit():
    faker = Faker()
    return faker.bs().split()[0]


def get_owner():
    faker = Faker()
    return faker.name()


def get_cost_center():
    faker = Faker()
    return faker.bban()[:5]


def get_csv_input_template(data):
    return {
        "source-dimension": data.replace("group-rule", "condition-value"),
        "dimension-1": data,
        "dimension-2": data,
        "dimension-3": data,
    }


def get_csv_input_example(business_unit, owner, cost_center):
    return {"Account": get_account_id(), "BusinessUnit": business_unit, "Owner": owner, "CostCenter": cost_center}


def get_costformation_template():
    return {"Dimensions": {}}


def get_dimension_template(name, source_dimension_name, normalize=True):
    dimension_template = {
        "Name": str(name),
        "Source": str(source_dimension_name),
        "Transforms": [{"Type": "Normalize"}],
        "Rules": [],
    }

    if not normalize:
        del dimension_template["Transforms"]

    return dimension_template


def get_group_rule_template(
    name: str,
    condition: bool = False,
    from_date: str | None = None,
    until_date: str | None = None,
) -> dict:
    """Create a group rule template with optional date range condition.

    Args:
        name: Name of the group rule
        condition: If True, use Contains instead of Equals
        from_date: Optional start date for ForDateRange (YYYY-MM-DD)
        until_date: Optional end date for ForDateRange (YYYY-MM-DD)

    Returns:
        Group rule dictionary with conditions
    """
    condition_type = "Contains" if condition else "Equals"
    rule = {"Type": "Group", "Name": str(name), "Conditions": [{condition_type: []}]}

    # Add ForDateRange condition if both dates are provided
    date_condition = get_date_range_condition(from_date or "", until_date or "")
    if date_condition:
        rule["Conditions"].append(date_condition)

    return rule


def get_mapping_template():
    template_rows = []

    for i in range(10):
        template_rows.append(get_csv_input_template(f"group-rule-{i + 1}"))

    template_column_names = list(template_rows[0].keys())

    return template_rows, template_column_names


def get_mapping_example():
    example_rows = []

    cost_center = get_cost_center()
    business_unit = get_business_unit()
    owner = get_owner()

    for i in range(10):
        if i % 4 == 0:
            cost_center = get_cost_center()
        if i % 2 == 0:
            business_unit = get_business_unit()
            owner = get_owner()

        example_rows.append(get_csv_input_example(business_unit, owner, cost_center))

    example_column_names = list(example_rows[0].keys())

    return example_rows, example_column_names


def normalize_name(name):
    for c in ".,/#!$%^&*;:=_~()'":
        name = str(name).replace(c, "-")

    return name.lower()


def display_conversion_summary(
    costformation: dict,
    mapping_name: str,
    output_file: str,
    rows_processed: int,
) -> None:
    """Display a visual summary of the conversion results."""
    console = Console()

    # Calculate statistics
    dimensions_count = len(costformation.get("Dimensions", {}))
    total_rules = 0
    total_members = 0
    total_date_ranges = 0
    dimension_stats = []

    for dim_name, dimension in costformation.get("Dimensions", {}).items():
        rules = dimension.get("Rules", [])
        rules_count = len(rules)
        members_count = 0
        date_range_count = 0

        for rule in rules:
            conditions = rule.get("Conditions", [])
            for cond in conditions:
                if "Equals" in cond:
                    members_count += len(cond["Equals"])
                elif "Contains" in cond:
                    members_count += len(cond["Contains"])
                elif "ForDateRange" in cond:
                    date_range_count += 1

        total_rules += rules_count
        total_members += members_count
        total_date_ranges += date_range_count
        dimension_stats.append((dim_name, rules_count, members_count, date_range_count))

    # Create title
    title = Text("CloudZero Cost Formation Mapping Conversion Complete", style="bold cyan")

    # Create summary panel content
    summary_lines = [
        f"[bold green]>[/bold green] Successfully processed [bold]{rows_processed}[/bold] rows from CSV",
        f"[bold green]>[/bold green] Created [bold]{dimensions_count}[/bold] dimension{'s' if dimensions_count != 1 else ''}",
        f"[bold green]>[/bold green] Generated [bold]{total_rules}[/bold] grouping rules",
        f"[bold green]>[/bold green] Mapped [bold]{total_members}[/bold] total members",
    ]
    if total_date_ranges > 0:
        summary_lines.append(
            f"[bold green]>[/bold green] Applied [bold]{total_date_ranges}[/bold] date range conditions"
        )

    summary_lines.extend(
        [
            "",
            f"[bold]Input:[/bold]  {mapping_name}",
            f"[bold]Output:[/bold] {output_file}",
        ]
    )
    summary_content = "\n".join(summary_lines)

    # Display main summary
    console.print("\n")
    console.print(Panel(summary_content, title=title, border_style="cyan"))

    # Create dimensions breakdown table
    if dimension_stats:
        table = Table(title="Dimensions Breakdown", show_header=True, header_style="bold magenta")
        table.add_column("Dimension Name", style="cyan", no_wrap=True)
        table.add_column("Group Rules", justify="right", style="green")
        table.add_column("Total Members", justify="right", style="yellow")
        if total_date_ranges > 0:
            table.add_column("Date Ranges", justify="right", style="blue")

        for dim_name, rules_count, members_count, date_range_count in dimension_stats:
            if total_date_ranges > 0:
                table.add_row(dim_name, str(rules_count), str(members_count), str(date_range_count))
            else:
                table.add_row(dim_name, str(rules_count), str(members_count))

        console.print("\n")
        console.print(table)

    # Add helpful information
    info_panel = Panel(
        """[bold]What was created:[/bold]
• A CloudZero Cost Formation YAML file that maps source account IDs to business dimensions
• Each dimension contains grouping rules that categorize your cloud resources
• These mappings help organize cloud costs by business units, teams, or custom categories

[bold]Next steps:[/bold]
1. Review the generated YAML file for accuracy
2. Upload to CloudZero using the Cost Formation UI or API
3. View your newly organized cost data in CloudZero dashboards""",
        title="💡 About Your Cost Formation Rules",
        border_style="blue",
    )

    console.print("\n")
    console.print(info_panel)
    console.print("\n")


def add_member(group_rule, member_name, group_rule_condition):
    group_rule["Conditions"][0][f"{'Contains' if group_rule_condition else 'Equals'}"].append(str(member_name))


def get_dimension_names(row: dict) -> tuple[list[str], str]:
    """Extract dimension names and source dimension from CSV row.

    Excludes reserved date columns (From, Until) from dimension names.

    Args:
        row: CSV row dictionary

    Returns:
        Tuple of (dimension_names, source_dimension_name)
    """
    source_dimension_name = str(list(row.keys())[0])
    dimension_names = [str(name) for name in row if name and name not in RESERVED_DATE_COLUMNS][1:]

    return dimension_names, source_dimension_name


def generate_template():
    template_rows, template_column_names = get_mapping_template()

    try:
        mapping_template_file_name = "mapping_template.csv"

        with open(mapping_template_file_name, mode="w") as csv_file:
            writer = csv.DictWriter(csv_file, fieldnames=template_column_names)
            writer.writeheader()

            for row in template_rows:
                writer.writerow(row)

        print(f'Mapping template saved to "./{mapping_template_file_name}"!')

    except Exception as err:
        print(f"Something went wrong: {err}")
        exit(-1)


def generate_example():
    example_rows, example_column_names = get_mapping_example()

    try:
        mapping_example_file_name = "mapping_example.csv"

        with open(mapping_example_file_name, mode="w") as csv_file:
            writer = csv.DictWriter(csv_file, fieldnames=example_column_names)
            writer.writeheader()

            for row in example_rows:
                writer.writerow(row)

            print(f'Mapping example saved to "./{mapping_example_file_name}"!')

    except Exception as err:
        print(f"Something went wrong: {err}")
        exit(-1)


def convert_mapping(arguments: dict) -> None:
    """Convert a CSV mapping file to CloudZero CostFormation YAML.

    Supports optional From/Until columns for ForDateRange conditions.

    Args:
        arguments: CLI arguments dictionary
    """
    try:
        config = parse_arguments(arguments)
    except ValueError as err:
        print(f"Error: {err}")
        exit(-1)

    context = MappingContext(costformation=get_costformation_template())

    try:
        with open(config.input_file, encoding="utf-8-sig") as csv_file:
            for row in csv.DictReader(csv_file):
                _process_row(row, context, config)
    except FileNotFoundError:
        print(f"Error: File not found: {config.input_file}")
        exit(-1)
    except csv.Error as err:
        print(f"Error parsing CSV: {err}")
        exit(-1)

    if context.rows_processed == 0:
        print("Error: No data rows found in CSV file")
        exit(-1)

    output_file = resolve_output_path(config.input_file, config.output_file)

    try:
        write_yaml(context.costformation, output_file)
    except OSError as err:
        print(f"Error writing output file: {err}")
        exit(-1)

    display_conversion_summary(context.costformation, config.input_file, output_file, context.rows_processed)


def _process_row(row: dict, context: MappingContext, config: ConversionConfig) -> None:
    """Process a single CSV row and update the conversion context.

    Args:
        row: CSV row dictionary
        context: Conversion state context
        config: Conversion configuration
    """
    context.rows_processed += 1

    # Initialize dimension names on first row
    if not context.source_dimension:
        dimension_names, context.source_dimension = get_dimension_names(row)
        if len(dimension_names) < 1 or not context.source_dimension:
            print("1 source dimension and at least 1 dimension required")
            print('Run "czops cf mapping_template" for details')
            exit(-1)
        context.dimension_names = dimension_names
    else:
        dimension_names = context.dimension_names

    # Extract date range values if present
    from_date = row.get("From", "").strip()
    until_date = row.get("Until", "").strip()

    condition_type = "Contains" if config.use_contains else "Equals"

    for dimension_name in dimension_names:
        if dimension_name not in context.group_rules:
            context.group_rules[dimension_name] = {}

        if not row.get(dimension_name):
            continue

        # Get canonical rule name (dedupe via normalized name)
        raw_rule_name = row[dimension_name]
        normalized_name = normalize_name(raw_rule_name)

        if normalized_name in context.normalized_names:
            rule_name = context.normalized_names[normalized_name]
        else:
            rule_name = raw_rule_name
            context.normalized_names[normalized_name] = raw_rule_name

        # Use tuple key to support same rule with different date ranges
        rule_key = (rule_name, from_date, until_date)

        if rule_key not in context.group_rules[dimension_name]:
            context.group_rules[dimension_name][rule_key] = get_group_rule_template(
                rule_name,
                condition=config.use_contains,
                from_date=from_date,
                until_date=until_date,
            )

        group_rule = context.group_rules[dimension_name][rule_key]

        # Add member to rule
        member_name = (
            normalize_name(row[context.source_dimension]) if config.normalize else row[context.source_dimension]
        )

        if member_name and member_name not in group_rule["Conditions"][0][condition_type]:
            add_member(group_rule, member_name, config.use_contains)

        # Ensure dimension exists in costformation
        if dimension_name not in context.costformation["Dimensions"]:
            context.costformation["Dimensions"][dimension_name] = get_dimension_template(
                dimension_name, context.source_dimension, normalize=config.normalize
            )

        dimension = context.costformation["Dimensions"][dimension_name]
        if group_rule not in dimension["Rules"]:
            dimension["Rules"].append(group_rule)
