# 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
#

import csv
import sys
from collections import Counter
from contextlib import suppress
from dataclasses import dataclass
from datetime import UTC, datetime, timedelta
from pathlib import Path
from shutil import get_terminal_size
from typing import Any

from rich import box
from rich.console import Console
from rich.panel import Panel
from rich.pretty import pprint
from rich.table import Table

from src.constants import CONNECTION_PROPERTIES, CONNECTION_TYPES, NO_ACCOUNT_NAME, NO_ORG_NAME
from src.utils.connections import ConnectionUtils, cur_type_display
from src.utils.ui import HealthStatus, PanelFactory, ProgressFactory, TableFactory

# CUR Feature mappings - map AdditionalSchemaElements to single-character abbreviations
CUR_FEATURE_MAPPINGS = {
    "RESOURCES": "R",  # Resource IDs
    "RESOURCE_TAGS": "T",  # Tags
    "SPLIT_COST_ALLOCATION_DATA": "S",  # Split cost allocation
    "MANUAL_DISCOUNT_COMPATIBILITY": "M",  # Manual discount compatibility
    "AUTOMATION": "A",  # Automation
    "ENHANCED_LINEITEM_PRICING": "E",  # Enhanced line item pricing
}

# Source name -> short acronym shown in the Src column
SOURCE_ACRONYMS: dict[str, str] = {
    "reactor-accounts": "RA",
    "connections-gcp": "GCP",
    "connections-azure": "AZ",
    "aws-organizations-discovery": "AOD",
    "snowflake-customer-data": "SCD",
    "healthcheck-lite": "HCL",
    "billing-connections": "BC",
    "billing-ingest": "BI",
}


def _source_acronym(source: str) -> str:
    """Return short display acronym for a data source name.

    Known sources use the curated SOURCE_ACRONYMS mapping. Unknown sources fall
    back to initials of hyphen-separated words (e.g. 'some-new-service' -> 'SNS'),
    which avoids the collision risk of a bare 3-char prefix and handles empty
    strings gracefully (an empty source produces '?' as a sentinel).
    """
    if source in SOURCE_ACRONYMS:
        return SOURCE_ACRONYMS[source]
    parts = [p for p in source.split("-") if p]
    if not parts:
        return "?"
    return "".join(p[0].upper() for p in parts)


def _format_source_display(record_sources: list[str] | None) -> str:
    """Format a record_sources list as compact comma-separated acronyms.

    Empty or None lists render as a dim dash. Empty-string entries within a
    non-empty list are skipped.
    """
    if not record_sources:
        return "[dim]-[/]"
    acronyms = [_source_acronym(s) for s in sorted(record_sources) if s]
    if not acronyms:
        return "[dim]-[/]"
    return ",".join(acronyms)


def classify_discovery_stub(connection: dict[str, Any]) -> None:
    """Mark never-connected records materialized by AWS Organizations discovery as discovered.

    The backend stamps ``is_discovered`` on every record that appears in the AWS
    Organizations enumeration — including real, connected accounts it enriches with
    discovery metadata. A record is a never-connected stub only when discovery is
    additionally its sole record source. Sets ``account_state="discovered"`` in place;
    real connections are left untouched.
    """
    if connection.get("account_state"):
        return
    sources = set(connection.get("record_sources") or [])
    if connection.get("is_discovered") and sources == {"aws-organizations-discovery"}:
        connection["account_state"] = "discovered"


# Clock-skew tolerance for freshness: a just-written record may carry a timestamp
# slightly ahead of the local clock; anything further in the future is bad data.
_FRESHNESS_SKEW_TOLERANCE = timedelta(minutes=5)

# A record within this window of its DynamoDB TTL deadline is about to expire.
_FRESHNESS_EXPIRY_WINDOW = timedelta(hours=6)


def _freshness_timestamp(connection: dict[str, Any]) -> str | None:
    """Pick the timestamp the Fresh column keys on: the materializer's last verification.

    ``last_materialized_at`` moves on every materializer write (content change, staleness
    rewrite, reconcile, discovery cycle), so it reflects "this record is still being
    produced". Records lacking it (client-synthesized rows) have no verification time.
    """
    return connection.get("last_materialized_at")


def _format_freshness_display(verified_at: str | None, ttl: float | str | None = None) -> str:
    """Render a graduated freshness icon from the record's last verification and TTL.

    Brightness scales with recency: bright ● within 6 hours, ● within 24 hours, dim ●
    within 3 days, ○ beyond that. A record less than 6 hours from its DynamoDB ``ttl``
    deadline renders empty — it is about to age out entirely. ``verified_at`` is the
    materializer's last verification pass (``last_materialized_at``): Fresh means "the
    backend recently confirmed this record", not "the record's content recently changed".
    Records without a timestamp (client-synthesized rows), with an unparseable one, or
    with a timestamp in the future beyond clock-skew tolerance render a dim dash.
    """
    if not verified_at:
        return "[dim]-[/]"
    try:
        updated = datetime.fromisoformat(str(verified_at).replace("Z", "+00:00"))
    except ValueError:
        return "[dim]-[/]"
    if updated.tzinfo is None:
        updated = updated.replace(tzinfo=UTC)

    now = datetime.now(UTC)

    if ttl is not None:
        # An unparseable TTL falls through to the recency ladder
        with suppress(ValueError, TypeError, OverflowError):
            expires = datetime.fromtimestamp(float(ttl), tz=UTC)
            if expires - now < _FRESHNESS_EXPIRY_WINDOW:
                return ""

    age = now - updated
    if age < -_FRESHNESS_SKEW_TOLERANCE:
        return "[dim]-[/]"
    if age <= timedelta(hours=6):
        return "[bold bright_green]●[/]"
    if age <= timedelta(hours=24):
        return "[green]●[/]"
    if age <= timedelta(days=3):
        return "[dim green]●[/]"
    return "[dim]○[/]"


def _format_elapsed(verified_at: str | None) -> str:
    """Elapsed time since ``verified_at`` as ``Nd Nh Nm`` (up to 3 largest non-zero units).

    Returns "" for a missing/unparseable timestamp or one in the future beyond clock skew
    (the same cases where the Fresh dot renders a dash), so the caller can append nothing.
    Sub-minute ages render "<1m".
    """
    if not verified_at:
        return ""
    try:
        updated = datetime.fromisoformat(str(verified_at).replace("Z", "+00:00"))
    except ValueError:
        return ""
    if updated.tzinfo is None:
        updated = updated.replace(tzinfo=UTC)
    delta = datetime.now(UTC) - updated
    if delta < -_FRESHNESS_SKEW_TOLERANCE:
        return ""
    total_minutes = max(0, int(delta.total_seconds() // 60))
    if total_minutes == 0:
        return "<1m"
    days, rem = divmod(total_minutes, 1440)
    hours, minutes = divmod(rem, 60)
    parts = [(days, "d"), (hours, "h"), (minutes, "m")]
    return " ".join(f"{value}{unit}" for value, unit in parts if value)


def _format_fresh_cell(connection: dict[str, Any]) -> str:
    """Render the Fresh column: the graduated dot plus elapsed-since-verified (``● 4d 12h 30m``).

    The elapsed suffix is appended only when the dot represents a real, dated record — the
    dash/blank (missing-timestamp, future-skew, TTL-expiring) cases show the dot alone.
    """
    verified_at = _freshness_timestamp(connection)
    dot = _format_freshness_display(verified_at, connection.get("ttl"))
    elapsed = _format_elapsed(verified_at)
    if not elapsed or dot in ("[dim]-[/]", ""):
        return dot
    return f"{dot} [dim]{elapsed}[/]"


def _build_source_legend(seen_sources: set[str]) -> str:
    """Build a compact one-line legend mapping acronyms to full source names."""
    if not seen_sources:
        return ""
    parts = [f"{_source_acronym(s)}={s}" for s in sorted(seen_sources)]
    return "Src: " + "  ".join(parts)


# Get actual terminal width or default to 180 if detection fails
try:
    terminal_width = get_terminal_size().columns
except OSError:
    # Fallback in case terminal size detection fails
    terminal_width = 180

# Use the full terminal width
# Initialize console with dynamic width
console = Console(width=None, highlight=False)


# Error/health flags that must reflect ANY active type: a multi-type connection with
# an error in one type's data (but not another) is not healthy. These OR across types
# rather than taking the first type's value.
_MERGE_OR_KEYS = frozenset({"permission_errors", "resource_ingest_permissions_errors"})


def _merged_type_data(connection: dict[str, Any]) -> dict[str, Any]:
    """Merge every ``types.{TYPE}.data`` dict into one flat view.

    The v4 contract nests operational detail (``link_health``, ``provisioning_process``,
    ``permission_errors``, ``resource_collector_configurations``, CUR config, spend, …)
    under the per-type ``data`` blocks — these fields do not exist at the record's top
    level, so reads must go through this merge. Boolean error/health flags in
    ``_MERGE_OR_KEYS`` are OR-ed across every type (an error in any active type counts);
    all other keys take the first type's value.
    """
    merged: dict[str, Any] = {}
    for type_config in (connection.get("types") or {}).values():
        for key, value in ((type_config or {}).get("data") or {}).items():
            if key in _MERGE_OR_KEYS:
                merged[key] = bool(merged.get(key)) or bool(value)
            else:
                merged.setdefault(key, value)
    return merged


def _get_connection_composite_key(connection: dict[str, Any]) -> str:
    """
    Generate a composite key for a connection to handle cases where
    the same connection_id might appear with different types.

    Keys on connection_id plus the sorted active ``connection_types``. Discovery stubs
    carry no type at all and key as ``<id>|-`` — previously they raised here, which
    aborted the entire org listing for any org containing a single stub.
    """
    conn_id = connection.get("connection_id")
    if not conn_id:
        raise ValueError(f"Missing connection_id for composite key: {connection!r:.120}")

    types = connection.get("connection_types") or []
    return f"{conn_id}|{','.join(sorted(types)) or '-'}"


@dataclass
class ConnectionMetrics:
    total_connections: int = 0
    discovered_accounts: int = 0
    malformed: int = 0
    enabled_orgs: int = 0
    disabled_orgs: int = 0
    cloud_providers: set[str] = None
    stages: Counter = None

    def __post_init__(self):
        self.cloud_providers = set()
        self.stages = Counter()


class ConnectionManager:
    def __init__(self, organization_data=None):
        self.metrics = ConnectionMetrics()
        self.organization_data = organization_data or {}
        self._column_visible: tuple[bool, ...] = tuple(True for _ in self.CONNECTION_COLUMNS)

    def _create_organization_header(
        self, org_name: str, org_id: str, org_stage: str, org_enabled: bool = True
    ) -> Panel:
        """Create organization header panel."""
        return PanelFactory.create_organization_header_panel(
            org_name=org_name, org_id=org_id, org_stage=org_stage, org_enabled=org_enabled
        )

    # CSV export fieldnames — must match the keys produced by _prepare_csv_row exactly,
    # or csv.DictWriter raises on the first row.
    CSV_FIELDNAMES: tuple[str, ...] = (
        "cz_organization_id",
        "cz_organization_name",
        "connection_name",
        "connected_date",
        "status",
        "account_type",
        "properties",
        "role_healthy",
        "ingest_healthy",
        "provisioned",
        "provider",
        "cur_type",
        "cur_type_collision",
        "connection_id",
    )

    # Connection table columns: (header, table kwargs, minimum console width to include).
    # Narrow terminals progressively drop lower-priority columns instead of wrapping
    # every cell into unreadable multi-line rows.
    CONNECTION_COLUMNS: tuple[tuple[str, dict, int], ...] = (
        ("", {"justify": "center"}, 0),  # Status icon only
        ("Connection Name", {"justify": "left", "no_wrap": False, "overflow": "fold"}, 0),
        ("Connection ID", {"justify": "left", "no_wrap": False, "overflow": "fold"}, 0),
        ("Provider", {"justify": "left", "no_wrap": False, "overflow": "fold"}, 120),
        ("Connection Type", {"justify": "left", "no_wrap": False, "overflow": "fold"}, 100),
        ("Health", {"justify": "left", "no_wrap": False, "overflow": "fold"}, 0),
        ("Src", {"justify": "left", "no_wrap": True}, 100),  # Record source acronym(s)
        ("CUR Type", {"justify": "left", "no_wrap": True}, 130),  # cur_type CONST (⚠ = collision)
        ("Fresh", {"justify": "left", "no_wrap": True}, 0),  # Dot + elapsed-since-verified
        ("First Seen", {"justify": "left", "no_wrap": False, "overflow": "fold"}, 150),
        ("30d Spend", {"justify": "right", "no_wrap": False, "overflow": "fold"}, 120),
    )

    def _create_connection_table(self, org_title: str) -> Table:
        """Create the connection table with columns fitting the current terminal width."""
        width = console.size.width
        self._column_visible = tuple(width >= min_width for _, _, min_width in self.CONNECTION_COLUMNS)
        columns = [
            (header, opts)
            for (header, opts, _), visible in zip(self.CONNECTION_COLUMNS, self._column_visible, strict=False)
            if visible
        ]

        return TableFactory.create_connections_table(
            title=org_title,
            columns=columns,
            width=None,  # Use full terminal width
        )

    def _get_connection_status(self, connection: dict[str, Any]) -> tuple[list[dict], list[dict]]:
        """Process and return connection status."""
        try:
            if connection.get("malformed"):
                return (
                    [
                        {
                            "code": "BAD",
                            "name": "BAD",
                            "healthy": False,
                            "display": "⚠️ [bright_red]BAD[/]",
                            "description": "Malformed connection record",
                        }
                    ],
                    [],
                )

            connection_type_status = []
            connection_prop_status = []

            # Process connection types
            for _k, v in CONNECTION_TYPES.items():
                if v["type_key"] in connection.get("connection_types", []):
                    # The v4 contract carries per-type health directly on TypeConfig
                    # (types.{TYPE}.healthy) for every provider.
                    healthy = ((connection.get("types") or {}).get(v["type_key"]) or {}).get("healthy")

                    connection_type_status.append(
                        {
                            "code": v["code"],
                            "name": v["name"],
                            "healthy": healthy,
                            "display": f"[bright_green]{v['code']}[/]" if healthy else f"[bright_red]{v['code']}[/]",
                            "description": v["description"],
                        }
                    )

            # Process connection properties
            for _k, v in CONNECTION_PROPERTIES.items():
                if self._should_add_property(connection, v):
                    connection_prop_status.append(
                        {
                            "code": v["code"],
                            "name": v["name"],
                            "healthy": True,
                            "display": f"[deep_sky_blue1]{v['code']}[/]",
                            "description": v["description"],
                        }
                    )

            return connection_type_status, connection_prop_status

        except Exception as error:
            console.print(
                f"[red]Error processing status for '{connection.get('connection_name')} ({connection.get('connection_id')})':[/] {error}"
            )
            return [], []

    def _should_add_property(self, account: dict[str, Any], property_config: dict[str, Any]) -> bool:
        """Determine if a property should be added to the status."""
        data = _merged_type_data(account)
        if account.get(property_config["enabled_key"]) or data.get(property_config["enabled_key"]):
            return True

        if provisioning_keywords := property_config.get("provisioning_keywords"):
            if provisioning_keywords[0] == "!" and data.get("provisioning_process") not in provisioning_keywords[1:]:
                return True

        return False

    def export_to_csv(
        self, output_file: str, connection_data: dict[str, Any], organization_data: dict[str, Any]
    ) -> None:
        """Export connection data to CSV file."""
        headers = self.CSV_FIELDNAMES

        try:
            output_path = Path(output_file)
            record_count = 0

            with ProgressFactory.create_export_progress() as progress:
                export_task = progress.add_task(
                    f"[cyan]Exporting to {output_file}...",
                    total=sum(len(conns) for conns in connection_data.values() if conns),
                )

                with output_path.open("w", newline="") as csv_fp:
                    writer = csv.DictWriter(csv_fp, fieldnames=headers, dialect="excel", quoting=csv.QUOTE_ALL)
                    writer.writeheader()

                    for org_id, connections_dict in connection_data.items():
                        if not connections_dict:
                            continue

                        org_name = organization_data.get(org_id, {}).get("cz_organization_name", NO_ORG_NAME)

                        for connection in connections_dict.values():
                            row = self._prepare_csv_row(org_id, org_name, connection)
                            writer.writerow(row)
                            record_count += 1
                            progress.advance(export_task)

            console.print(f"[green]Export complete: {record_count} records written to {output_file}")

        except OSError as error:
            console.print(f"[red]Error writing to CSV file: {error}")
            sys.exit(1)
        except Exception as error:
            console.print(f"[red]Unexpected error during CSV export: {error}")
            sys.exit(1)

    def _prepare_csv_row(self, org_id: str, org_name: str, connection: dict[str, Any]) -> dict[str, Any]:
        """Prepare a single row for CSV export with plain text (no formatting, colors, or emojis)."""
        c_type_objs, c_props_objs = self._get_connection_status(connection)

        # Extract plain text codes for types and properties without rich formatting
        c_type = [obj["code"] for obj in c_type_objs]
        c_props = [obj["code"] for obj in c_props_objs]

        return {
            "cz_organization_id": org_id,
            "cz_organization_name": org_name,
            "connection_name": f'="{connection.get("connection_name", NO_ACCOUNT_NAME)}"',
            "connected_date": connection.get("created"),
            "status": (
                "discovered"
                if connection.get("account_state") == "discovered"
                else ("connected" if connection.get("is_connected") else "disconnected")
            ),
            "account_type": ",".join(c_type) if c_type else "",
            "properties": ",".join(c_props) if c_props else "",
            "role_healthy": connection.get("is_healthy"),
            "ingest_healthy": not bool(_merged_type_data(connection).get("resource_ingest_permissions_errors")),
            "provisioned": _merged_type_data(connection).get("provisioning_process") or "legacy",
            "provider": connection.get("provider"),
            "cur_type": connection.get("cur_type") or "",
            "cur_type_collision": bool(connection.get("cur_type_collision")),
            "connection_id": f'="{connection["connection_id"]}"',
        }

    def print_connection_list(self, organization_data: dict[str, Any], connection_data: dict[str, Any]):
        """Print formatted connection list with improved organization display."""
        # Reset metrics for accurate counting
        self.metrics = ConnectionMetrics()

        # Store organization data for use in other methods
        self.organization_data = organization_data or {}

        # Process each organization
        with ProgressFactory.create_spinner_progress(
            "Processing organizations", total=len(connection_data), console=console
        ) as progress:
            task = progress.add_task("Processing...", total=len(connection_data))

            # Sort organizations by name for consistent display
            sorted_orgs = sorted(
                [(cz_org_id, organization_data.get(cz_org_id, {})) for cz_org_id in connection_data],
                key=lambda x: x[1].get("cz_organization_name", "").lower(),
            )

            for cz_org_id, org_info in sorted_orgs:
                connections_dict = connection_data.get(cz_org_id, {})
                if not connections_dict:
                    continue

                # Process organization information
                org_name = org_info.get("cz_organization_name", "").capitalize() or cz_org_id
                org_enabled = org_info.get("organization_enabled", True)
                org_stage = org_info.get("organization_stage", "unknown")

                self._update_metrics(org_enabled, org_stage)

                # Process connections (organization header is now created within this method)
                self._process_connections(connections_dict, org_name, cz_org_id, org_stage)

                progress.advance(task)

        # Print summary after all organizations have been processed
        self._print_summary()

    def _update_metrics(self, org_enabled: bool, org_stage: str):
        """Update metrics based on organization information."""
        self.metrics.stages[f"stage-{org_stage}"] += 1
        if org_enabled:
            self.metrics.enabled_orgs += 1
        else:
            self.metrics.disabled_orgs += 1

    def _process_connections(self, connections_dict: dict[str, Any], org_name: str, org_id: str, org_stage: str):
        """Process and display connections for an organization with improved formatting."""
        connections = list(connections_dict.values()) if connections_dict else []
        for connection in connections:
            classify_discovery_stub(connection)

        # Get organization enabled status from the organization data
        org_info = self.organization_data.get(org_id, {})
        # Handle both Pydantic models and dicts
        if hasattr(org_info, "model_dump"):
            org_info = org_info.model_dump()
        org_enabled = org_info.get("organization_enabled", True)

        # Sort connections by status (Connected first, then Partial, then Disconnected, then Discovered)
        sorted_connections = sorted(
            connections,
            key=lambda c: (
                c.get("account_state") == "discovered",
                not c.get("is_connected", False),
                bool(_merged_type_data(c).get("permission_errors", False)),
            ),
        )

        # Create a more descriptive table title
        discovered_count = sum(1 for c in connections if c.get("account_state") == "discovered")
        connection_count = len(connections) - discovered_count

        table_title = f"{connection_count} Connections"
        if discovered_count:
            table_title += f", {discovered_count} Discovered"

        # Collect unique record sources across all connections for the legend
        seen_sources: set[str] = set()
        for conn in connections:
            for src in conn.get("record_sources") or []:
                seen_sources.add(src)

        # Create and populate the connection table
        connection_table = self._create_connection_table(table_title)

        if not connections:
            connection_table.add_row("[yellow]No connections found[/]", *[""] * (sum(self._column_visible) - 1))
        else:
            for connection in sorted_connections:
                self._add_connection_row(connection_table, connection)

        # Print the organization information and connections table
        console.print()
        console.print(self._create_organization_header(org_name, org_id, org_stage, org_enabled))
        console.print(connection_table)

        if seen_sources:
            console.print(f"[dim]{_build_source_legend(seen_sources)}[/dim]")

        console.print()

    def _add_connection_row(self, table: Table, connection: dict[str, Any]):
        """Add a connection row to the table with improved formatting."""
        # Compute is_connected if not present
        if "is_connected" not in connection:
            connection["is_connected"] = connection.get("enabled", True) and connection.get("is_healthy", True)
        is_discovered = connection.get("account_state") == "discovered"
        if is_discovered:
            self.metrics.discovered_accounts += 1
        else:
            self.metrics.total_connections += 1
        self.metrics.cloud_providers.add(connection.get("provider", "unknown"))

        # Connection Name
        connection_name_display = connection.get("connection_name") or "[dim]-[/]"

        # Connection ID
        account_id_display = connection.get("connection_id", "")

        # Connection Type - show all types from connection_types list
        connection_types = connection.get("connection_types", [])
        connection_type_display = ", ".join(connection_types) if connection_types else "[dim]-[/]"

        # Health status - simplified
        health_display = self._get_health_summary(connection)

        # Provider - show the actual cloud provider with color coding
        provider_raw = connection.get("provider") or ""
        if provider_raw:
            provider_upper = provider_raw.upper()
            if provider_upper == "AWS":
                provider_display = "[orange1]AWS[/]"
            elif provider_upper == "GCP":
                # Rainbow effect for GCP - using different colors for each letter
                provider_display = "[red]G[/][yellow]C[/][blue]P[/]"
            elif provider_upper == "AZURE":
                provider_display = "[blue]AZURE[/]"
            elif provider_upper == "SNOWFLAKE":
                provider_display = "[cornflower_blue]SNOWFLAKE[/]"
            else:
                provider_display = provider_raw
        else:
            provider_display = "[dim]-[/]"

        # First Seen - format as YYYY-MM-DD HH:MM TZ
        # For discovered accounts the AWS-Organizations join date is more meaningful
        # than when the discovery job first materialized the record.
        if is_discovered:
            first_seen = (connection.get("discovery_metadata") or {}).get("joined_timestamp") or connection.get(
                "first_seen"
            )
        else:
            first_seen = connection.get("first_seen")
        if first_seen and first_seen != "None":
            try:
                # Parse ISO8601 datetime and format as requested
                from datetime import datetime

                if isinstance(first_seen, str):
                    # Try to parse the ISO format
                    dt = datetime.fromisoformat(first_seen.replace("Z", "+00:00"))
                    first_seen_display = dt.strftime("%Y-%m-%d %H:%M %Z") or dt.strftime("%Y-%m-%d %H:%M UTC")
                else:
                    first_seen_display = str(first_seen)
            except Exception:
                # If parsing fails, just use the raw value
                first_seen_display = str(first_seen)
        else:
            first_seen_display = "[dim]-[/]"

        # 30-day spend - stored in types.COST.data.last_30_days_billed_spend
        cost_data = (connection.get("types") or {}).get("COST", {}).get("data", {})
        spend_value = cost_data.get("last_30_days_billed_spend")

        # A zero/absent/unknown spend renders blank (no $0.00, no dash) — only a
        # positive amount is shown.
        spend_display = ""
        if not is_discovered and spend_value:
            try:
                spend_float = float(spend_value) if isinstance(spend_value, str) else spend_value
                if spend_float > 0:
                    spend_display = f"${spend_float:,.2f}"
            except (ValueError, TypeError):
                spend_display = ""

        # Record source acronym(s)
        source_display = _format_source_display(connection.get("record_sources"))

        # CUR Type CONST (short name); a ⚠ marks a CUR-type collision on the payer account
        cur_type_cell = cur_type_display(connection.get("cur_type"), None)
        if connection.get("cur_type_collision"):
            cur_type_cell = f"[yellow]{cur_type_cell} ⚠[/]"

        # Build row data (Provider before Connection Type)
        row_data = [
            self._get_connection_status_display(connection),
            connection_name_display,
            account_id_display,
            provider_display,
            connection_type_display,
            health_display,
            source_display,
            cur_type_cell,
            _format_fresh_cell(connection),
            first_seen_display,
            spend_display,
        ]

        table.add_row(*[value for value, visible in zip(row_data, self._column_visible, strict=False) if visible])

    def _get_connection_status_display(self, connection: dict[str, Any]) -> str:
        """Get the display status icon only for a connection."""
        return HealthStatus.get_connection_status_icon(connection)

    def _get_health_summary(self, connection: dict[str, Any]) -> str:
        """Get a simplified health summary for the connection."""
        if connection.get("account_state") == "discovered":
            return "[bright_cyan]Discovered[/]"

        is_healthy = connection.get("is_healthy", False)
        is_connected = connection.get("is_connected")
        if is_connected is None:
            is_connected = connection.get("enabled", True) and is_healthy
        has_permission_errors = _merged_type_data(connection).get("permission_errors", False)

        if not is_connected:
            return "[red]Disconnected[/]"
        elif not is_healthy:
            return "[yellow]Unhealthy[/]"
        elif has_permission_errors:
            return "[yellow]Partial[/]"
        else:
            return "[green]Healthy[/]"

    def _get_simplified_resource_display(self, connection: dict[str, Any]) -> str:
        """Get simplified resource collection display."""
        rc_configs = connection.get("resource_collector_configurations")
        if not rc_configs:
            return "[dim]-[/]"

        parts = []
        if rc_configs.get("cwm", False):
            parts.append("[green]CWM[/]")

        if probes := rc_configs.get("probes", []):
            # Show count and first few probe names
            if len(probes) <= 3:
                parts.append(f"[blue]{','.join(probes)}[/]")
            else:
                parts.append(f"[blue]{len(probes)} probes[/]")

        return " ".join(parts) if parts else "[dim]Enabled[/]"

    def _print_summary(self):
        """Print compact summary information."""
        if self.metrics.total_connections > 0:
            # Simple one-line summary
            providers = sorted(self.metrics.cloud_providers)
            provider_text = ", ".join(providers)

            summary_text = f"[cyan]{self.metrics.total_connections}[/cyan] connections from {len(self.metrics.cloud_providers)} providers ({provider_text})"
            if self.metrics.discovered_accounts:
                summary_text += f", [bright_cyan]{self.metrics.discovered_accounts}[/bright_cyan] discovered"

            legend_text = "🟢=Healthy 🟡=Partial 🔴=Disconnected"
            if self.metrics.discovered_accounts:
                legend_text += " 🔵=Discovered"
            legend_text += (
                " ●=Verified(brighter=newer) ○=Unverified>3d blank=Expiring<6h -=No timestamp ⚠=CUR collision | "
            )

            console.print(f"\n{summary_text}")
            console.print(f"[dim]{legend_text}[/dim]\n")


def create_organization_summary(organization_data: dict[str, Any], connection_data: dict[str, dict[str, Any]]):
    """Create a summary table of organizations and their connections with dynamic column widths."""
    from shutil import get_terminal_size

    from rich.table import Table

    # Detect terminal width with a safe fallback
    try:
        term_width = get_terminal_size().columns
    except OSError:
        term_width = 120

    # Establish a minimum usable width and guardrails for very narrow terminals
    min_width = 80
    effective_width = max(term_width, min_width)

    # Compute dynamic column widths proportionally
    # Fixed columns: Org ID (no_wrap), Total (5, no_wrap)
    # The rest are proportional to remaining space
    fixed_total_col_width = 5
    # Reserve some space (padding/borders). Rich will handle borders, but keep a safety margin
    safety_padding = 6

    remaining = max(effective_width - fixed_total_col_width - safety_padding, 40)

    # Proportions for each column (must sum to 1.0)
    # Organization, Payer Connections, CUR Features, [30 Day Spend], Resource Collection
    org_ratio = 0.22
    payer_ratio = 0.38
    cur_ratio = 0.10
    spend_ratio = 0.15
    rc_ratio = 0.15

    # Calculate widths
    org_w = max(18, int(remaining * org_ratio))
    payer_w = max(24, int(remaining * payer_ratio))
    cur_w = max(10, int(remaining * cur_ratio))
    spend_w = max(12, int(remaining * spend_ratio))
    rc_w = max(14, int(remaining * rc_ratio))

    # Create summary table
    summary_table = Table(
        title="Organization Connections Summary",
        title_style="bold cyan",
        title_justify="left",
        box=box.ROUNDED,
        header_style="bold white",
        width=None,  # Use full terminal width
    )

    # Define columns with dynamic widths
    summary_table.add_column("Org ID", no_wrap=True)
    summary_table.add_column("Organization", width=org_w, overflow="fold")
    summary_table.add_column("Payer Connections", width=payer_w, overflow="fold")
    summary_table.add_column("CUR Features", width=cur_w, overflow="fold")
    summary_table.add_column("30 Day Cloud Spend", justify="right", width=spend_w, overflow="fold")
    summary_table.add_column("Resource Collection", width=rc_w, overflow="fold")
    summary_table.add_column("Total", justify="right", width=fixed_total_col_width, no_wrap=True)

    # Sort organizations by name
    sorted_orgs = sorted(organization_data.items(), key=lambda x: x[1].get("cz_organization_name", "").lower())

    # Process each organization (now sorted by name)
    for org_id, org_info in sorted_orgs:
        org_connections = connection_data.get(org_id, {})

        if not org_connections:
            continue

        # Organization name and ID
        org_name = org_info.get("cz_organization_name", "Unknown")
        org_info.get("organization_enabled", True)

        # Format org status display

        # Connection counts
        connection_count = len(org_connections)

        # Identify payer accounts
        payer_accounts = []

        # 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
        #

        # Variables for health calculations
        healthy_count = 0
        unhealthy_count = 0

        # Variables for resource collection tracking
        resource_collection_enabled_count = 0
        cwm_enabled_count = 0
        probes_enabled_count = 0
        probe_types = set()

        # Variables for CUR features tracking
        cur_features = set()  # Track all CUR features across payer accounts

        # Variable for 30 day spend tracking
        total_30_day_spend = 0.0

        for _conn_id, conn in org_connections.items():
            is_payer = "COST" in conn.get("connection_types", [])

            if is_payer:
                payer_accounts.append(conn.get("connection_id", "Unknown"))

                # Extract CUR features for payer accounts
                if cur_metadata := _merged_type_data(conn).get("cur_metadata"):
                    # Get the active CUR report name
                    active_cur = _merged_type_data(conn).get("master_payer_billing_report_name")
                    if active_cur and active_cur in cur_metadata:
                        # Get the AdditionalSchemaElements from the active CUR
                        if schema_elements := cur_metadata[active_cur].get("AdditionalSchemaElements", []):
                            for element in schema_elements:
                                if element in CUR_FEATURE_MAPPINGS:
                                    cur_features.add(CUR_FEATURE_MAPPINGS[element])

            # Calculate health status
            is_healthy = conn.get("is_healthy", False)
            is_connected = conn.get("is_connected", False)
            has_permission_errors = _merged_type_data(conn).get("permission_errors", False)

            if is_connected and is_healthy and not has_permission_errors:
                healthy_count += 1
            elif is_connected:
                # This counts partial connections (connected but with issues)
                unhealthy_count += 1

            # Track resource collection status - determined solely by the presence of resource_collector_configurations
            if rc_configs := _merged_type_data(conn).get("resource_collector_configurations"):
                resource_collection_enabled_count += 1

                # Check for CloudWatch Metrics
                if rc_configs.get("cwm", False):
                    cwm_enabled_count += 1

                # Check for probes
                if probes := rc_configs.get("probes", []):
                    probes_enabled_count += 1
                    # Track all probe types
                    for probe in probes:
                        probe_types.add(probe)

            # Check for last_30_days_billed_spend in types.COST.data
            cost_data = (conn.get("types") or {}).get("COST", {}).get("data", {})
            if spend_str := cost_data.get("last_30_days_billed_spend"):
                try:
                    total_30_day_spend += float(spend_str)
                except (ValueError, TypeError):
                    pass

        # Determine health icon based on status
        health_icon = HealthStatus.determine_overall_health_icon(
            healthy_count=healthy_count, unhealthy_count=unhealthy_count, total_count=connection_count
        )

        # Create combined connections info display
        connections_info = ConnectionUtils.format_connection_info_list(
            payer_accounts=payer_accounts, org_connections=org_connections
        )

        # If no payers found
        if not connections_info:
            connections_info = ["None"]

        # 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
        #

        connections_display = health_icon + " " + " | ".join(connections_info)

        # Create CUR features display - sort features alphabetically for consistent display
        if cur_features:
            # Map each feature to its color
            feature_colors = {
                "R": "green",  # Resources
                "T": "blue",  # Tags
                "S": "yellow",  # Split Cost Allocation
                "M": "magenta",  # Manual Discount
                "A": "cyan",  # Automation
                "E": "red",  # Enhanced Pricing
            }

            # Build display with individual colors
            colored_features = []
            for feature in sorted(cur_features):
                color = feature_colors.get(feature, "cyan")  # Default to cyan if unknown
                colored_features.append(f"[{color}]{feature}[/]")

            cur_features_display = "".join(colored_features)
        else:
            cur_features_display = "[dim]None[/]"

        # Create resource collection display
        if resource_collection_enabled_count == 0:
            rc_display = "[dim]None[/]"
        else:
            rc_display = f"[green]{resource_collection_enabled_count}/{connection_count}[/]"

            # Add CloudWatch metrics status if any are enabled
            if cwm_enabled_count > 0:
                rc_display += f" [green]CWM:{cwm_enabled_count}[/]"

            # Add probe status if any are enabled
            if probes_enabled_count > 0:
                probe_text = ", ".join(sorted(probe_types)) if probe_types else ""
                if len(probe_text) > 15:
                    # Truncate if too many probe types
                    probe_text = probe_text[:12] + "..."
                rc_display += f" [blue]P:{probes_enabled_count}[/]"

        # Format 30-day spend for display
        spend_display = f"[green]${total_30_day_spend:,.2f}[/]" if total_30_day_spend > 0 else "[dim]$0.00[/]"

        # Add row to the table
        row_data = [
            org_id,  # Now first column (never truncated)
            org_name,
            connections_display,
            cur_features_display,  # CUR features column
            spend_display,  # 30 day spend column
            rc_display,  # Resource collection status
            str(connection_count),  # Now last column
        ]

        summary_table.add_row(*row_data)

    # Print the summary
    console.print()
    console.print(
        PanelFactory.create_welcome_panel(
            description="[bold]CloudZero Connection Management[/bold]\nOrganization-level summary of connections",
            title="Connection Summary",
            style="green",
        )
    )
    console.print(summary_table)
    console.print()


# 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
#


def display_connections_for_organization(
    org_id: str, organization_data: dict[str, Any], connection_data: dict[str, Any], show_header: bool = True
):
    """
    Reusable function to display connections for a single organization.

    This function is used by both 'czops conn list' and 'czops org info' to ensure
    consistent connection display across commands.

    Args:
        org_id: Organization ID
        organization_data: Dictionary containing organization information
        connection_data: Dictionary of connections for the organization
        show_header: Whether to show the organization header (default: True)
    """
    manager = ConnectionManager(organization_data)

    org_info = organization_data.get(org_id, {})
    # Handle both Pydantic models and dicts
    if hasattr(org_info, "model_dump"):
        org_info = org_info.model_dump()
    org_name = org_info.get("cz_organization_name", "").capitalize() or org_id
    org_stage = org_info.get("organization_stage", "unknown")

    # Process and display connections
    manager._process_connections(connection_data, org_name, org_id, org_stage)


def list_functions(arguments: dict[str, Any], organization_data: dict[str, Any] | None = None):
    """Main function to list and process connections."""
    manager = ConnectionManager(organization_data)

    # Parse limit argument
    limit = arguments.get("--limit")
    if limit:
        limit = int(limit)

    if arguments.get("--age"):
        console.print("[yellow]Age filtering is currently not supported[/]")
        return

    if arguments.get("--list-disabled"):
        console.print("[cyan]Including disabled organizations in account list[/]")

    if arguments.get("--debug"):
        pprint(arguments)
        pprint(organization_data)

    # Fetch and process connections
    with ProgressFactory.create_fetch_progress("Fetching connections data") as progress:
        task = progress.add_task("Fetching connections data...", status="")
        result = _fetch_connections(arguments, organization_data, progress, task)

        # Handle both tuple (connections_data, org_data) and dict (just connections_data) returns
        if isinstance(result, tuple):
            connection_data, fetched_org_data = result
            # Update organization_data with fetched data if we got any
            if fetched_org_data:
                if organization_data is None:
                    organization_data = fetched_org_data
                else:
                    organization_data.update(fetched_org_data)
        else:
            connection_data = result

    # Apply limit to connections (flatten across orgs, apply limit, re-group)
    if limit and connection_data:
        # Flatten all connections into a list with their org_id
        flat_connections = []
        for org_id, org_connections in connection_data.items():
            for conn_key, conn_data in org_connections.items():
                flat_connections.append((org_id, conn_key, conn_data))

        # Apply limit
        flat_connections = flat_connections[:limit]

        # Re-group by org_id
        connection_data = {}
        for org_id, conn_key, conn_data in flat_connections:
            if org_id not in connection_data:
                connection_data[org_id] = {}
            connection_data[org_id][conn_key] = conn_data

    # Handle raw JSON output (actual JSON, machine-readable)
    if arguments.get("--raw"):
        import json

        print(json.dumps(connection_data, indent=2, default=str))
        return

    # Handle formatted dict output (human-readable via pprint)
    if arguments.get("--json"):
        pprint(connection_data)
        return

    if output_file := arguments.get("--output"):
        manager.export_to_csv(output_file, connection_data, organization_data)
    else:
        # Check if organization ID/name was specified - handle both string and list formats
        org_id_or_name = arguments.get("<ORG-ID-or-NAME>")
        has_org_specified = org_id_or_name and (
            isinstance(org_id_or_name, str) or (isinstance(org_id_or_name, list) and org_id_or_name)
        )

        # If no specific organization was provided, show the summary view
        if not has_org_specified:
            create_organization_summary(organization_data, connection_data)
        else:
            manager.print_connection_list(organization_data, connection_data)


# 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
#


def _fetch_connections(arguments, organization_data, progress, task):
    """Fetch connection data based on arguments using v4 API."""
    from src.api.v4_connections import get_connections_for_org
    from src.utils.connections import health_filter, resolve_cur_filters, status_filter
    from src.utils.validation import looks_like_an_organization_id

    # Update progress information
    progress.update(task, description="Fetching connections data...")

    try:
        debug = arguments.get("--debug", False)
        is_healthy = health_filter(arguments)
        is_connected = status_filter(arguments)
        cur_type, cur_type_collision, cur_type_error = resolve_cur_filters(arguments, debug=debug)
        if cur_type_error:
            console.print(f"[red]Error: {cur_type_error}[/]")
            return {}, {}
        connections_data = {}

        # Get organization ID if specified - handle both string and list formats
        org_id_or_name = arguments.get("<ORG-ID-or-NAME>")

        # Convert list to string if needed (CLI sometimes passes a list instead of a string)
        if isinstance(org_id_or_name, list) and org_id_or_name:
            org_id_or_name = org_id_or_name[0]

        # An organization is required — a fleet-wide listing (no org) is not supported: the
        # global /connections endpoint is unbounded and times out (cz-ops-services#437).
        # docopt already requires the positional; this guards an empty/whitespace value.
        if not org_id_or_name or not str(org_id_or_name).strip():
            console.print("[red]Error: an organization is required.[/]")
            console.print("  [white]czops connection list <ORG-ID-or-NAME>[/]")
            return {}, {}

        # Check if input is a UUID (org ID) or a name/search term
        if org_id_or_name and not looks_like_an_organization_id(org_id_or_name):
            # Not a UUID - treat as organization name and look it up first
            progress.update(task, description=f"Looking up organization '{org_id_or_name}'...")

            try:
                from src.features.organizations import get_organization_data

                # Look up the organization by name (same as czops org info does)
                fetched_org_data = get_organization_data(org_id_or_name, arguments)

                if not fetched_org_data or len(fetched_org_data) == 0:
                    console.print(f"[yellow]No organization found matching '[cyan]{org_id_or_name}[/]'[/]")
                    return {}, {}

                # Get the org ID from the fetched organization data
                org_id = list(fetched_org_data.keys())[0]
                org_name = fetched_org_data[org_id].get("cz_organization_name", org_id_or_name)

                progress.update(task, description=f"Loading connections for organization '{org_name}'...")

                # Now fetch connections for this organization ID
                matching_connections = get_connections_for_org(
                    org_id,
                    is_healthy=is_healthy,
                    is_connected=is_connected,
                    cur_type=cur_type,
                    cur_type_collision=cur_type_collision,
                    debug=debug,
                )

                if matching_connections:
                    # Store connections under the org_id
                    connections_data[org_id] = {}
                    for conn in matching_connections:
                        # Use composite key to handle duplicate connection_ids with different types
                        conn_dict = conn.model_dump()
                        composite_key = _get_connection_composite_key(conn_dict)
                        connections_data[org_id][composite_key] = conn_dict

                    progress.update(
                        task,
                        description=f"Found {len(matching_connections)} connections for organization '{org_name}'",
                        status="✓",
                    )

                    # Return both connections_data and organization_data
                    return connections_data, fetched_org_data
                else:
                    console.print(f"[yellow]No connections found for organization '[cyan]{org_name}[/]' ({org_id})[/]")

                return connections_data, fetched_org_data

            except Exception as e:
                console.print(f"[red]Error searching connections: {e}[/]")
                return {}, {}

        if org_id_or_name and looks_like_an_organization_id(org_id_or_name):
            # Specific organization requested by ID
            # If organization_data is missing this org, fetch it
            if not organization_data or org_id_or_name not in organization_data:
                progress.update(task, description=f"Loading organization data for {org_id_or_name}...")
                from src.features.organizations import get_organization_data

                try:
                    fetched_org_data = get_organization_data(org_id_or_name, arguments)
                    if fetched_org_data:
                        if organization_data is None:
                            organization_data = fetched_org_data
                        else:
                            organization_data.update(fetched_org_data)
                    else:
                        # Org not found, create minimal entry
                        if organization_data is None:
                            organization_data = {}
                        organization_data[org_id_or_name] = {
                            "cz_organization_id": org_id_or_name,
                            "cz_organization_name": org_id_or_name[:8] + "…",
                            "organization_enabled": True,
                        }
                except Exception as org_err:
                    if debug:
                        console.print(f"[yellow]Warning: Could not fetch org data: {org_err}[/]")
                    # Create minimal entry so we can still try to fetch connections
                    if organization_data is None:
                        organization_data = {}
                    organization_data[org_id_or_name] = {
                        "cz_organization_id": org_id_or_name,
                        "cz_organization_name": org_id_or_name[:8] + "…",
                        "organization_enabled": True,
                    }

            # Now fetch connections for the specific org
            org_ids = [org_id_or_name]

            for org_id in org_ids:
                progress.update(task, description=f"Loading connections for organization {org_id}...")

                # Get organization info for context
                org_info = organization_data.get(org_id, {}) if organization_data else {}
                expected_connections = org_info.get("connection_count", "unknown")
                org_name = org_info.get("cz_organization_name", "Unknown")

                try:
                    connections_list = get_connections_for_org(
                        org_id,
                        is_healthy=is_healthy,
                        is_connected=is_connected,
                        cur_type=cur_type,
                        cur_type_collision=cur_type_collision,
                        debug=debug,
                    )

                    org_connections = {}
                    for conn in connections_list:
                        conn_dict = conn.model_dump()
                        composite_key = _get_connection_composite_key(conn_dict)
                        org_connections[composite_key] = conn_dict

                    if org_connections:
                        connections_data[org_id] = org_connections

                except Exception as e:
                    if "502" in str(e) or "Internal server error" in str(e):
                        console.print(
                            "[red]Error: V4 connections API is experiencing server issues (502). Please try again later or contact support.[/]"
                        )
                        console.print(
                            f"[dim]Organization '{org_name}' ({org_id}) has {expected_connections} connections according to metadata, but connection details cannot be fetched.[/]"
                        )
                        console.print("[dim]This is a temporary server-side issue with the v4 connections endpoint.[/]")
                    else:
                        console.print(f"[yellow]Warning: Could not fetch connections for organization {org_id}: {e}[/]")
                    continue

        # Update progress
        total_orgs = len(connections_data)
        total_connections = sum(len(conns) for conns in connections_data.values())
        progress.update(
            task,
            description="Connections data retrieved",
            status=f"Found {total_connections} connections across {total_orgs} organizations",
        )
        progress.advance(task)

        console.print(
            f"[green]Retrieved information for {total_connections} connections across {total_orgs} organizations using v4 API[/]"
        )

        # Return tuple if organization_data was modified, otherwise just connections_data for backward compatibility
        if organization_data:
            return connections_data, organization_data
        return connections_data

    except Exception as error:
        console.print(f"[red]Error fetching connections: {error}")
        return {}


# Additional helper functions would go here...
