"""
CDF Records API integration for posting validation results.
"""

import time
from typing import Any

_VIOLATION_SEVERITY = "violation"


def _is_retryable_error(error: Exception) -> bool:
    """Check if an error is retryable (5xx server errors or network issues)."""
    error_str = str(error)
    # Retry on 5xx server errors
    if "500" in error_str or "502" in error_str or "503" in error_str or "504" in error_str:
        return True
    # Retry on gateway/timeout errors
    if "timeout" in error_str.lower() or "gateway" in error_str.lower():
        return True
    return False


def _post_with_retry(client: Any, url: str, payload: dict, max_retries: int = 3, initial_delay: float = 1.0) -> None:
    """
    Post to CDF API with exponential backoff retry for transient errors.

    Args:
        client: CogniteClient instance
        url: API endpoint URL
        payload: JSON payload to post
        max_retries: Maximum number of retry attempts (default: 3)
        initial_delay: Initial delay in seconds before first retry (default: 1.0)

    Raises:
        Exception: If all retries exhausted or non-retryable error encountered
    """
    last_error = None

    for attempt in range(max_retries):
        try:
            client.post(url, json=payload)
            return  # Success
        except Exception as e:
            last_error = e

            # Don't retry on 4xx client errors (bad request, auth, etc)
            if not _is_retryable_error(e):
                raise

            # Last attempt - raise the error
            if attempt == max_retries - 1:
                raise

            # Exponential backoff with jitter
            delay = initial_delay * (2**attempt)
            print(f"    Retryable error (attempt {attempt + 1}/{max_retries}): {e}")
            print(f"    Retrying in {delay:.1f}s...")
            time.sleep(delay)

    # Should never reach here, but raise last error just in case
    raise last_error  # type: ignore[misc]


def post_records_batch(
    client,
    records: list[dict],
    stream_id: str,
    batch_size: int = 1000,
    max_retries: int = 3,
) -> tuple[int, list[dict]]:
    """
    Post pre-built records to CDF Records API with batching and retry logic.

    Args:
        client: CogniteClient instance
        records: List of pre-built record dicts ready to post
        stream_id: Records API stream ID
        batch_size: Number of records per batch (default: 1000)
        max_retries: Number of retry attempts for transient errors (default: 3)

    Returns:
        Tuple of (posted_count, errors)
    """
    if not records:
        return 0, []

    # Enable alpha version for Records API
    original_headers = client.config.headers.copy()
    client.config.headers["cdf-version"] = "alpha"

    posted_count = 0
    errors = []
    records_url = f"/api/v1/projects/{client.config.project}/streams/{stream_id}/records"

    try:
        for i in range(0, len(records), batch_size):
            batch = records[i : i + batch_size]
            batch_payload = {"items": batch}
            batch_num = i // batch_size + 1

            print(f"  Posting batch {batch_num} ({len(batch)} records)...")

            try:
                # Try batch post with retry
                _post_with_retry(client, records_url, batch_payload, max_retries=max_retries)
                posted_count += len(batch)
                print(f"  ✓ Posted batch {batch_num}: {len(batch)} records")
            except Exception as batch_error:
                # If batch fails after retries, try individual records
                print(f"  ✗ Batch {batch_num} failed after retries, trying individually: {batch_error}")

                for record_item in batch:
                    try:
                        # Try individual post with retry
                        _post_with_retry(
                            client,
                            records_url,
                            {"items": [record_item]},
                            max_retries=max_retries,
                        )
                        posted_count += 1
                    except Exception as individual_error:
                        # Individual record failed after retries - log and continue
                        errors.append(
                            {"record": record_item.get("externalId", "unknown"), "error": str(individual_error)}
                        )
                        print(
                            f"  ✗ ERROR posting record {record_item.get('externalId', 'unknown')}: {individual_error}"
                        )

    finally:
        # Restore original headers
        client.config.headers = original_headers

    return posted_count, errors


def post_validation_results_to_records(
    client,
    all_instances: list[dict],
    violations: list[dict],
    job_run_id: str,
    stream_id: str,
    rule_set_id: str,
    rule_set_version: str,
    namespace_base: str,
    records_space: str = "dataQuality",
    records_container: str = "DataQualityValidationRecord",
) -> tuple[int, list[dict]]:
    """
    Post SHACL validation results to CDF Records API.

    Creates one record per validated instance:
    - Instances with violations get failedConstraints populated
    - Instances without violations get empty failedConstraints (passed)

    Returns:
        Tuple of (posted_count, errors)
    """
    # Group violations by focus node
    violations_by_instance: dict[str, list] = {}
    for violation in violations:
        focus_node = violation.get("focusNode", "unknown")
        if focus_node not in violations_by_instance:
            violations_by_instance[focus_node] = []
        violations_by_instance[focus_node].append(violation)

    # Build a mapping of all instances to their violations (or empty list if passed)
    instances_to_post = {}
    for inst in all_instances:
        inst_space = inst.get("space", "unknown")
        inst_external_id = inst.get("externalId", "unknown")

        # Build the focus node URI as used in SHACL validation
        # For DMS instances: {namespace_base}{space}/{externalId}
        # For RAW rows: {namespace_base}{externalId} (space is empty)
        if inst_space and inst_space != "":
            focus_node = f"{namespace_base}{inst_space}/{inst_external_id}"
        else:
            focus_node = f"{namespace_base}{inst_external_id}"

        instances_to_post[focus_node] = {
            "space": inst_space,
            "externalId": inst_external_id,
            "violations": violations_by_instance.get(focus_node, []),
        }

    passed_count = sum(1 for i in instances_to_post.values() if not i["violations"])
    failed_count = sum(1 for i in instances_to_post.values() if i["violations"])

    print(f"Posting results for {len(instances_to_post)} instances to Records API...")
    print(f"  - Passed: {passed_count}")
    print(f"  - Failed: {failed_count}")

    # Enable alpha version for Records API
    original_headers = client.config.headers.copy()
    client.config.headers["cdf-version"] = "alpha"

    # Build all records first
    records_to_post = []

    for focus_node, inst_data in instances_to_post.items():
        external_id = inst_data["externalId"]
        instance_space = inst_data["space"]
        instance_violations = inst_data["violations"]

        # Build failed constraints list (empty for passed instances)
        failed_constraints = []
        source_shapes = set()
        dimensions: set[str] = set()
        target_classes: set[str] = set()
        constraint_details = []
        severities = []  # Collect severities to determine worst

        for v in instance_violations:
            constraint_component = v.get("sourceConstraintComponent", "Unknown")
            result_path = v.get("resultPath")

            # Build constraint string with component and path (format: "component::path")
            if result_path:
                constraint_str = f"{constraint_component}::{result_path}"
            else:
                constraint_str = constraint_component

            failed_constraints.append(constraint_str)

            if v.get("sourceShape"):
                source_shapes.add(v["sourceShape"])

            if v.get("dimension"):
                dimensions.add(v["dimension"])

            if v.get("targetClass"):
                target_classes.add(v["targetClass"])

            # Include violation details (excluding fields that have their own top-level properties)
            raw_severity = v.get("resultSeverity", "Violation")
            severity = str(raw_severity).split("#")[-1] if "#" in str(raw_severity) else str(raw_severity)
            severities.append(severity)

            detail = {
                "sourceConstraintComponent": constraint_component,
                "resultMessage": v.get("resultMessage", "No message"),
                "resultSeverity": severity,
                "resultPath": v.get("resultPath"),
                "value": v.get("value"),
                "sourceConstraint": v.get("sourceConstraint"),
                "dimension": v.get("dimension"),
            }
            # Remove None values to keep JSON clean
            detail = {k: v for k, v in detail.items() if v is not None}

            constraint_details.append(detail)

        # Create record for this instance
        record_external_id = f"dq_{rule_set_id}_{external_id}_{job_run_id}"

        # Build properties dict
        properties = {
            "ruleSetId": rule_set_id,
            "ruleSetVersion": rule_set_version,
            "jobRunId": job_run_id,
            "passedValidation": not any(s.lower() == _VIOLATION_SEVERITY for s in severities),
            "resultSeverity": severities,  # list of text: ["Violation", "Warning", ...]
            "failedConstraints": failed_constraints,  # Empty array = passed, non-empty = failed
            "focusNode": focus_node,
            "validationReport": {
                "violationCount": len(instance_violations),
                "violations": constraint_details,
                "summary": f"{len(instance_violations)} violation(s)"
                if instance_violations
                else "Passed all constraints",
            },
        }

        # Only include focusNodeInstance for DMS instances (when space is not empty)
        # For RAW validation, space is empty so we skip this field
        if instance_space and instance_space != "":
            properties["focusNodeInstance"] = {"space": instance_space, "externalId": external_id}

        record_item: dict[str, Any] = {
            "space": records_space,
            "externalId": record_external_id,
            "sources": [
                {
                    "source": {"type": "container", "space": records_space, "externalId": records_container},
                    "properties": properties,
                }
            ],
        }

        # Add optional fields
        if source_shapes:
            record_item["sources"][0]["properties"]["sourceShape"] = list(source_shapes)

        if dimensions:
            record_item["sources"][0]["properties"]["dimensions"] = list(dimensions)

        if target_classes:
            record_item["sources"][0]["properties"]["targetClass"] = list(target_classes)

        records_to_post.append(record_item)

    # Post records in batches (max 1000 per batch for Records API)
    posted_count = 0
    errors = []
    batch_size = 1000
    records_url = f"/api/v1/projects/{client.config.project}/streams/{stream_id}/records"

    try:
        for i in range(0, len(records_to_post), batch_size):
            batch = records_to_post[i : i + batch_size]
            batch_payload = {"items": batch}
            batch_num = i // batch_size + 1

            print(f"  Posting batch {batch_num} ({len(batch)} records)...")

            try:
                # Try batch post with retry
                _post_with_retry(client, records_url, batch_payload, max_retries=3)
                posted_count += len(batch)
                print(f"  ✓ Posted batch {batch_num}: {len(batch)} records")
            except Exception as batch_error:
                # If batch fails after retries, try individual records
                print(f"  ✗ Batch {batch_num} failed after retries, trying individually: {batch_error}")

                for record_item in batch:
                    try:
                        # Try individual post with retry
                        _post_with_retry(
                            client,
                            records_url,
                            {"items": [record_item]},
                            max_retries=3,
                        )
                        posted_count += 1
                    except Exception as individual_error:
                        # Individual record failed after retries - log and continue
                        errors.append({"instance": record_item["externalId"], "error": str(individual_error)})
                        print(f"  ✗ ERROR posting record for {record_item['externalId']}: {individual_error}")

    finally:
        # Restore original headers
        client.config.headers = original_headers

    print(f"Posted {posted_count}/{len(instances_to_post)} records")
    if errors:
        print(f"Errors: {len(errors)}")
        for err in errors[:5]:
            print(f"  - {err['instance']}: {err['error']}")

    return posted_count, errors
