"""
Cognite Function for generating demo time series data for data quality testing.

This function creates 25 CogniteTimeSeries instances with various data quality
patterns and continuously inserts datapoints for testing SHACL validation rules.

On initial setup (when time series have no data), 1 year of historical data is generated.
On subsequent runs, only incremental data is added.

Time series patterns:
- good_*: Regular data with no quality issues
- outlier_*: Data with extreme outliers
- gap_*: Data with gaps (missing periods)
- decrease_*: Cumulative data with unexpected decreases
- sparse_*: Very low data density
- noisy_*: High variance/noise in values
"""

import datetime
import random
from typing import Any

# Constants
MINUTES_PER_YEAR = 365 * 24 * 60  # ~525,600 minutes
BATCH_SIZE = 10000  # Max datapoints per insert call


def _ensure_space_exists(client, space_id: str) -> None:
    """Ensure the space exists, create if not."""
    from cognite.client.data_classes.data_modeling import SpaceApply

    try:
        existing = client.data_modeling.spaces.retrieve(space_id)
        if existing:
            return
    except Exception:
        pass

    space = SpaceApply(
        space=space_id,
        name="Data Quality Test Space",
        description="Space for data quality validation testing",
    )
    client.data_modeling.spaces.apply(space)
    print(f"Created space: {space_id}")


def _ensure_timeseries_exists(client, space: str, external_id: str, name: str, description: str) -> bool:
    """
    Ensure a CogniteTimeSeries DMS instance exists.

    Returns:
        True if newly created, False if already existed
    """
    from cognite.client.data_classes.data_modeling import (
        NodeApply,
        NodeId,
        NodeOrEdgeData,
        ViewId,
    )

    view_id = ViewId("cdf_cdm", "CogniteTimeSeries", "v1")
    node_id = NodeId(space=space, external_id=external_id)

    # Check if it already exists
    try:
        existing = client.data_modeling.instances.retrieve(nodes=[node_id])
        if existing.nodes:
            return False  # Already exists
    except Exception:
        pass

    node = NodeApply(
        space=space,
        external_id=external_id,
        sources=[
            NodeOrEdgeData(
                source=view_id,
                properties={
                    "name": name,
                    "description": description,
                    "type": "numeric",
                    "isStep": False,
                },
            )
        ],
    )

    try:
        client.data_modeling.instances.apply(node)
        return True  # Newly created
    except Exception as e:
        print(f"  Warning creating {external_id}: {e}")
        return False


def _generate_good_data(
    base_value: float, num_points: int, start_time: datetime.datetime
) -> list[tuple[datetime.datetime, float]]:
    """Generate regular data with small variance - should pass all quality checks."""
    data = []
    for i in range(num_points):
        ts = start_time + datetime.timedelta(minutes=i)
        value = base_value + random.gauss(0, base_value * 0.02)  # 2% variance
        data.append((ts, value))
    return data


def _generate_outlier_data(
    base_value: float, num_points: int, start_time: datetime.datetime
) -> list[tuple[datetime.datetime, float]]:
    """Generate data with extreme outliers - should trigger outlier detection."""
    data = []
    for i in range(num_points):
        ts = start_time + datetime.timedelta(minutes=i)
        if random.random() < 0.05:  # 5% chance of outlier
            value = base_value * random.choice([5, 10, -3, 0.1])  # Extreme values
        else:
            value = base_value + random.gauss(0, base_value * 0.02)
        data.append((ts, value))
    return data


def _generate_gap_data(
    base_value: float, num_points: int, start_time: datetime.datetime
) -> list[tuple[datetime.datetime, float]]:
    """Generate data with gaps - should trigger gap detection."""
    data = []
    in_gap = False
    gap_end_idx = 0

    for i in range(num_points):
        ts = start_time + datetime.timedelta(minutes=i)

        # Handle gaps
        if in_gap and i < gap_end_idx:
            continue
        in_gap = False

        # Start new gap randomly (about every 1000 points on average)
        if random.random() < 0.001:
            gap_length = random.randint(120, 360)  # 2-6 hours
            in_gap = True
            gap_end_idx = i + gap_length
            continue

        value = base_value + random.gauss(0, base_value * 0.02)
        data.append((ts, value))
    return data


def _generate_decrease_data(
    base_value: float, num_points: int, start_time: datetime.datetime
) -> list[tuple[datetime.datetime, float]]:
    """Generate cumulative data with unexpected decreases - should trigger decrease check."""
    data = []
    cumulative = base_value
    for i in range(num_points):
        ts = start_time + datetime.timedelta(minutes=i)

        if random.random() < 0.0001:  # ~50 resets per year
            cumulative = cumulative * 0.5
        else:
            cumulative += random.uniform(0.1, 2.0)

        data.append((ts, cumulative))
    return data


def _generate_sparse_data(
    base_value: float, num_points: int, start_time: datetime.datetime
) -> list[tuple[datetime.datetime, float]]:
    """Generate very sparse data - should trigger low density warnings."""
    data = []
    # Only generate 10% of expected points, spaced 10 minutes apart
    sparse_points = num_points // 10
    for i in range(sparse_points):
        ts = start_time + datetime.timedelta(minutes=i * 10)
        value = base_value + random.gauss(0, base_value * 0.02)
        data.append((ts, value))
    return data


def _generate_noisy_data(
    base_value: float, num_points: int, start_time: datetime.datetime
) -> list[tuple[datetime.datetime, float]]:
    """Generate high variance/noisy data - should trigger out of range detection."""
    data = []
    for i in range(num_points):
        ts = start_time + datetime.timedelta(minutes=i)
        value = base_value + random.gauss(0, base_value * 0.5)  # 50% variance
        data.append((ts, value))
    return data


def _generate_empty_data(
    base_value: float, num_points: int, start_time: datetime.datetime
) -> list[tuple[datetime.datetime, float]]:
    """Return empty data - should trigger no data warnings."""
    return []


# Define the 25 time series configurations
TIMESERIES_CONFIGS = [
    # Good data (5 series) - should pass all checks
    {
        "prefix": "good",
        "index": 1,
        "generator": _generate_good_data,
        "base": 100,
        "desc": "Regular sensor - temperature",
    },
    {
        "prefix": "good",
        "index": 2,
        "generator": _generate_good_data,
        "base": 50,
        "desc": "Regular sensor - pressure",
    },
    {
        "prefix": "good",
        "index": 3,
        "generator": _generate_good_data,
        "base": 1000,
        "desc": "Regular sensor - flow rate",
    },
    {
        "prefix": "good",
        "index": 4,
        "generator": _generate_good_data,
        "base": 25,
        "desc": "Regular sensor - humidity",
    },
    {
        "prefix": "good",
        "index": 5,
        "generator": _generate_good_data,
        "base": 500,
        "desc": "Regular sensor - power",
    },
    # Outlier data (5 series) - should trigger outlier detection
    {
        "prefix": "outlier",
        "index": 1,
        "generator": _generate_outlier_data,
        "base": 100,
        "desc": "Sensor with outliers - temperature",
    },
    {
        "prefix": "outlier",
        "index": 2,
        "generator": _generate_outlier_data,
        "base": 50,
        "desc": "Sensor with outliers - pressure",
    },
    {
        "prefix": "outlier",
        "index": 3,
        "generator": _generate_outlier_data,
        "base": 1000,
        "desc": "Sensor with outliers - flow rate",
    },
    {
        "prefix": "outlier",
        "index": 4,
        "generator": _generate_outlier_data,
        "base": 25,
        "desc": "Sensor with outliers - humidity",
    },
    {
        "prefix": "outlier",
        "index": 5,
        "generator": _generate_outlier_data,
        "base": 500,
        "desc": "Sensor with outliers - power",
    },
    # Gap data (5 series) - should trigger gap detection
    {
        "prefix": "gap",
        "index": 1,
        "generator": _generate_gap_data,
        "base": 100,
        "desc": "Sensor with gaps - temperature",
    },
    {
        "prefix": "gap",
        "index": 2,
        "generator": _generate_gap_data,
        "base": 50,
        "desc": "Sensor with gaps - pressure",
    },
    {
        "prefix": "gap",
        "index": 3,
        "generator": _generate_gap_data,
        "base": 1000,
        "desc": "Sensor with gaps - flow rate",
    },
    {
        "prefix": "gap",
        "index": 4,
        "generator": _generate_gap_data,
        "base": 25,
        "desc": "Sensor with gaps - humidity",
    },
    {
        "prefix": "gap",
        "index": 5,
        "generator": _generate_gap_data,
        "base": 500,
        "desc": "Sensor with gaps - power",
    },
    # Decrease data (3 series) - should trigger value decrease check
    {
        "prefix": "decrease",
        "index": 1,
        "generator": _generate_decrease_data,
        "base": 1000,
        "desc": "Counter with resets - energy meter",
    },
    {
        "prefix": "decrease",
        "index": 2,
        "generator": _generate_decrease_data,
        "base": 5000,
        "desc": "Counter with resets - production count",
    },
    {
        "prefix": "decrease",
        "index": 3,
        "generator": _generate_decrease_data,
        "base": 100,
        "desc": "Counter with resets - runtime hours",
    },
    # Sparse data (3 series) - should trigger low density warnings
    {
        "prefix": "sparse",
        "index": 1,
        "generator": _generate_sparse_data,
        "base": 100,
        "desc": "Sparse sensor - temperature",
    },
    {
        "prefix": "sparse",
        "index": 2,
        "generator": _generate_sparse_data,
        "base": 50,
        "desc": "Sparse sensor - pressure",
    },
    {
        "prefix": "sparse",
        "index": 3,
        "generator": _generate_sparse_data,
        "base": 1000,
        "desc": "Sparse sensor - flow rate",
    },
    # Noisy data (2 series) - should trigger out of range detection
    {
        "prefix": "noisy",
        "index": 1,
        "generator": _generate_noisy_data,
        "base": 100,
        "desc": "Noisy sensor - temperature",
    },
    {
        "prefix": "noisy",
        "index": 2,
        "generator": _generate_noisy_data,
        "base": 50,
        "desc": "Noisy sensor - pressure",
    },
    # Empty data (2 series) - should trigger no data warnings
    {
        "prefix": "empty",
        "index": 1,
        "generator": _generate_empty_data,
        "base": 0,
        "desc": "Empty sensor - no data",
    },
    {
        "prefix": "empty",
        "index": 2,
        "generator": _generate_empty_data,
        "base": 0,
        "desc": "Empty sensor - disconnected",
    },
]


def _insert_datapoints_batched(client, instance_id, datapoints: list) -> int:
    """Insert datapoints in batches to avoid API limits."""
    total_inserted = 0
    for i in range(0, len(datapoints), BATCH_SIZE):
        batch = datapoints[i : i + BATCH_SIZE]
        client.time_series.data.insert(batch, instance_id=instance_id)
        total_inserted += len(batch)
    return total_inserted


def handle(data: dict[str, Any], client) -> dict[str, Any]:
    """
    Generate demo time series data for data quality testing.

    For NEW time series: Creates the time series and inserts historical data.
    For EXISTING time series: Skips historical data but inserts new incremental datapoints.

    Args:
        data: Input data containing:
            - space: Space ID for time series (default: ts_dq_test)
            - historical_days: Days of historical data for new time series (default: 365)
            - points_per_run: Number of new datapoints to insert per run (default: 1)
        client: Cognite client instance

    Returns:
        Dict with generation results
    """
    from cognite.client.data_classes.data_modeling import NodeId

    print("=" * 80)
    print("DEMO DATA GENERATOR")
    print("=" * 80)

    space = data.get("space", "ts_dq_test")
    historical_days = data.get("historical_days", 365)
    points_per_run = data.get("points_per_run", 1)

    print(f"Space: {space}")
    print(f"Historical data days: {historical_days}")
    print(f"Points per run: {points_per_run}")

    results = {
        "space": space,
        "timeseries_created": 0,
        "timeseries_existing": 0,
        "historical_points_inserted": 0,
        "incremental_points_inserted": 0,
        "errors": [],
    }

    # Ensure space exists
    print("\n[1/3] Ensuring space exists...")
    _ensure_space_exists(client, space)

    # Calculate timestamps
    now = datetime.datetime.now(tz=datetime.timezone.utc)
    historical_start = now - datetime.timedelta(days=historical_days)
    incremental_start = now - datetime.timedelta(minutes=points_per_run)
    historical_points = historical_days * 24 * 60  # 1 point per minute

    # Process each time series
    print(f"\n[2/3] Processing {len(TIMESERIES_CONFIGS)} time series...")

    for i, config in enumerate(TIMESERIES_CONFIGS, 1):
        external_id = f"dq_test_{config['prefix']}_{config['index']}"
        name = f"DQ Test - {config['prefix'].title()} {config['index']}"
        instance_id = NodeId(space=space, external_id=external_id)

        print(f"\n  [{i}/{len(TIMESERIES_CONFIGS)}] {external_id}")

        try:
            # Try to create the time series
            is_new = _ensure_timeseries_exists(client, space, external_id, name, config["desc"])

            if is_new:
                # Newly created - insert historical data
                results["timeseries_created"] += 1
                print(f"    NEW - generating {historical_points:,} historical points...")

                # Generate and insert historical data
                historical_data = config["generator"](config["base"], historical_points, historical_start)

                if historical_data:
                    inserted = _insert_datapoints_batched(client, instance_id, historical_data)
                    results["historical_points_inserted"] += inserted
                    print(f"    Inserted {inserted:,} historical points")
            else:
                results["timeseries_existing"] += 1
                print("    EXISTS - skipping historical data")

        except Exception as e:
            results["errors"].append({"timeseries": external_id, "error": str(e)})
            print(f"    Error: {e}")

    # Insert incremental datapoints for ALL time series (including empty pattern)
    print(f"\n[3/3] Inserting {points_per_run} new datapoint(s) per time series...")

    for config in TIMESERIES_CONFIGS:
        external_id = f"dq_test_{config['prefix']}_{config['index']}"
        instance_id = NodeId(space=space, external_id=external_id)

        try:
            # Generate incremental data
            incremental_data = config["generator"](config["base"], points_per_run, incremental_start)

            if incremental_data:
                client.time_series.data.insert(incremental_data, instance_id=instance_id)
                results["incremental_points_inserted"] += len(incremental_data)
                print(f"  {external_id}: +{len(incremental_data)}")
            else:
                print(f"  {external_id}: (empty)")

        except Exception as e:
            results["errors"].append({"timeseries": external_id, "error": str(e)})
            print(f"  {external_id}: Error - {e}")

    # Summary
    print("\n" + "=" * 80)
    print("SUMMARY")
    print("=" * 80)
    print(f"Time series created (with history): {results['timeseries_created']}")
    print(f"Time series existing (history skipped): {results['timeseries_existing']}")
    print(f"Historical points inserted: {results['historical_points_inserted']:,}")
    print(f"Incremental points inserted: {results['incremental_points_inserted']}")
    print(f"Errors: {len(results['errors'])}")

    if results["errors"]:
        print("\nErrors:")
        for err in results["errors"][:5]:
            print(f"  - {err['timeseries']}: {err['error']}")

    return results
