from __future__ import annotations

import urllib.parse
from collections import defaultdict
from collections.abc import Iterable
from typing import TYPE_CHECKING

from cognite.client.data_classes.data_modeling import (
    Boolean,
    ContainerApply,
    ContainerProperty,
    DirectRelation,
    Json,
    Text,
)
from cognite.client.exceptions import CogniteAPIError
from pydantic import BaseModel, Field

if TYPE_CHECKING:
    from cognite.client import CogniteClient

_VIOLATION_SEVERITY = "violation"


class RecordsConfig(BaseModel):
    """Records API configuration."""

    stream_id: str | None = None
    rule_set_id: str
    rule_set_version: str
    data_domain_external_id: str | None = None
    records_space: str = Field(default="dataQuality")
    records_container: str = Field(default="DataQualityValidationRecord")


class Violation(BaseModel):
    """Single validation violation from SHACL report."""

    focusNode: str | None = None
    resultMessage: str | None = None
    resultSeverity: str | None = None
    sourceConstraintComponent: str | None = None
    resultPath: str | None = None
    value: str | None = None
    sourceShape: str | None = None
    sourceConstraint: str | None = None
    dimension: str | None = None
    targetClass: str | None = None


class DataQualityRecord(BaseModel):
    """Record payload for a single validated instance."""

    space: str
    external_id: str
    properties: dict


def parse_focus_node_uri(
    uri: str,
    namespace_base: str = "http://purl.org/cognite/",
) -> tuple[str, str] | None:
    """Parse focusNode URI into (space, external_id), matching NEAT and simple formats.

    NEAT:   {namespace_base}space/{instance_space}#{urlencoded(external_id)}
    Simple:     {namespace_base}{space}/{external_id}
    """
    if not uri or not uri.startswith(namespace_base):
        return None
    suffix = uri[len(namespace_base) :].rstrip("/")
    if not suffix:
        return None
    if suffix.startswith("space/"):
        idx = suffix.find("#")
        if idx < 0:
            return None
        instance_space = suffix[6:idx].rstrip("/")
        external_id = urllib.parse.unquote(suffix[idx + 1 :].strip())
        return (instance_space, external_id) if instance_space and external_id is not None else None
    parts = suffix.split("/")
    if len(parts) >= 2:
        return (parts[0], parts[1])
    if len(parts) == 1:
        return ("unknown", parts[0])
    return None


def build_records(
    instances: Iterable[dict],
    violations: list[Violation],
    *,
    job_run_id: str,
    records_config: RecordsConfig,
    namespace_base: str,
) -> list[dict]:
    """Build Records API payloads for all instances."""
    violations_by_instance: dict[tuple[str, str], list[Violation]] = defaultdict(list)
    for violation in violations:
        parsed = parse_focus_node_uri(violation.focusNode or "", namespace_base)
        if parsed:
            violations_by_instance[parsed].append(violation)

    items: list[dict] = []
    for instance in instances:
        inst_space = instance.get("space", "unknown")
        inst_external_id = instance.get("externalId", "unknown")
        instance_key = (inst_space, inst_external_id)
        instance_violations = violations_by_instance.get(instance_key, [])
        focus_node = f"{namespace_base}{inst_space}/{inst_external_id}"

        failed_constraints: list[str] = []
        source_shapes: set[str] = set()
        constraint_details: list[dict] = []
        severities: list[str] = []
        dimensions: set[str] = set()
        target_classes: set[str] = set()

        for violation in instance_violations:
            constraint_component = violation.sourceConstraintComponent or "Unknown"
            result_path = violation.resultPath
            constraint_str = f"{constraint_component}::{result_path}" if result_path else constraint_component
            failed_constraints.append(constraint_str)

            if violation.sourceShape:
                source_shapes.add(violation.sourceShape)

            if violation.dimension:
                dimensions.add(violation.dimension)

            if violation.targetClass:
                target_classes.add(violation.targetClass)

            raw_severity = violation.resultSeverity or "Violation"
            severity = raw_severity.split("#")[-1] if "#" in raw_severity else raw_severity
            severities.append(severity)

            detail = {
                "sourceConstraintComponent": constraint_component,
                "resultMessage": violation.resultMessage or "No message",
                "resultSeverity": severity,
                "resultPath": violation.resultPath,
                "value": violation.value,
                "sourceConstraint": violation.sourceConstraint,
                "sourceShape": violation.sourceShape,
                "dimension": violation.dimension,
            }
            constraint_details.append({k: v for k, v in detail.items() if v is not None})

        record_external_id = f"dq_{records_config.rule_set_id}_{inst_external_id}_{job_run_id}"
        properties: dict[str, object] = {
            "ruleSetId": records_config.rule_set_id,
            "ruleSetVersion": records_config.rule_set_version,
            "jobRunId": job_run_id,
            "passedValidation": not any(s.lower() == _VIOLATION_SEVERITY for s in severities),
            "resultSeverity": severities,
            "failedConstraints": failed_constraints,
            "focusNode": focus_node,
            "focusNodeInstance": {"space": inst_space, "externalId": inst_external_id},
            "validationReport": {
                "violationCount": len(instance_violations),
                "violations": constraint_details,
                "summary": (
                    f"{len(instance_violations)} violation(s)" if instance_violations else "Passed all constraints"
                ),
            },
        }

        if source_shapes:
            properties["sourceShape"] = list(source_shapes)

        if dimensions:
            properties["dimensions"] = list(dimensions)

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

        if records_config.data_domain_external_id:
            properties["dataDomainExternalId"] = records_config.data_domain_external_id

        items.append(
            {
                "space": records_config.records_space,
                "externalId": record_external_id,
                "sources": [
                    {
                        "source": {
                            "type": "container",
                            "space": records_config.records_space,
                            "externalId": records_config.records_container,
                        },
                        "properties": properties,
                    }
                ],
            }
        )

    return items


def build_schema_issue_records(
    schema_issues: list,  # List of SchemaIssue from SHACL validation
    *,
    job_run_id: str,
    records_config: RecordsConfig,
    datamodel_space: str,
    datamodel_external_id: str,
    datamodel_version: str,
) -> list[dict]:
    """Build Records API payloads for schema inconsistencies.

    Args:
        schema_issues: List of SchemaIssue objects from SHACL validation
        job_run_id: Unique identifier for this validation run
        records_config: Records API configuration
        datamodel_space: Data model space being validated
        datamodel_external_id: Data model external ID
        datamodel_version: Data model version

    Returns:
        List of record payloads ready for Records API
    """
    if not schema_issues:
        return []

    items: list[dict] = []
    for idx, issue in enumerate(schema_issues):
        # Convert issue to dict if it has to_dict method, otherwise use __dict__
        issue_dict = issue.to_dict() if hasattr(issue, "to_dict") else issue.__dict__

        # Create unique record ID for schema issue
        issue_type = issue_dict.get("issue_type", "unknown")
        view_id = issue_dict.get("view_id", "unknown").replace("/", "_")
        property_name = issue_dict.get("property_name", "unknown")
        record_external_id = (
            f"schema_{records_config.rule_set_id}_{issue_type}_{view_id}_{property_name}_{job_run_id}_{idx}"
        )

        # Build properties for Records API
        properties: dict[str, object] = {
            "ruleSetId": records_config.rule_set_id,
            "ruleSetVersion": records_config.rule_set_version,
            "jobRunId": job_run_id,
            "passedValidation": False,  # Schema issues always mean validation didn't fully pass
            "resultSeverity": [issue_dict.get("severity", "Warning")],
            "failedConstraints": [f"schema::{issue_type}"],
            "focusNode": issue_dict.get("view_id", "unknown"),
            "validationReport": {
                "violationCount": 1,
                "violations": [
                    {
                        "issueType": issue_type,
                        "severity": issue_dict.get("severity", "Warning"),
                        "message": issue_dict.get("message", ""),
                        "viewId": issue_dict.get("view_id"),
                        "propertyName": issue_dict.get("property_name"),
                        "sourceViewId": issue_dict.get("source_view_id"),
                        "errorCode": issue_dict.get("error_code"),
                        "details": issue_dict.get("details", {}),
                    }
                ],
                "summary": issue_dict.get("message", "Schema inconsistency detected"),
            },
        }

        if records_config.data_domain_external_id:
            properties["dataDomainExternalId"] = records_config.data_domain_external_id

        # Add data model context to the validation report details (not as separate properties)
        properties["validationReport"]["dataModel"] = {  # type: ignore[index]
            "space": datamodel_space,
            "externalId": datamodel_external_id,
            "version": datamodel_version,
        }

        items.append(
            {
                "space": records_config.records_space,
                "externalId": record_external_id,
                "sources": [
                    {
                        "source": {
                            "type": "container",
                            "space": records_config.records_space,
                            "externalId": records_config.records_container,
                        },
                        "properties": properties,
                    }
                ],
            }
        )

    return items


def ensure_records_infrastructure(client: CogniteClient, records_config: RecordsConfig) -> None:
    """Ensure Records API stream and container exist."""
    if records_config.stream_id is None:
        return
    _ensure_records_stream(client, records_config.stream_id)
    _ensure_records_container(client, records_config.records_space, records_config.records_container)


def _ensure_records_stream(client: CogniteClient, stream_id: str) -> None:
    payload = {
        "items": [
            {
                "externalId": stream_id,
                "settings": {"template": {"name": "BasicLiveData"}},
            }
        ]
    }
    try:
        client.post(f"/api/v1/projects/{client.config.project}/streams", json=payload)
    except CogniteAPIError as exc:
        if exc.code != 409:
            raise


def _ensure_records_container(client: CogniteClient, space: str, container_id: str) -> None:
    """Ensure the Records API container exists with used_for='record' (required for ingestion).

    If the container already exists, it will be used as-is without modification.
    Container creation/modification requires schema WRITE permissions.
    """
    existing = None
    try:
        existing = client.data_modeling.containers.retrieve((space, container_id))
    except Exception:
        pass

    # If container exists, use it as-is (don't modify to avoid permission issues)
    if existing:
        return

    # Container doesn't exist - try to create it (requires schema WRITE permission)
    try:
        properties = {
            "ruleSetId": ContainerProperty(type=Text(), description="Identifier for the SHACL rule set"),
            "ruleSetVersion": ContainerProperty(type=Text(), description="Version of the rule set"),
            "jobRunId": ContainerProperty(type=Text(), description="Unique identifier for the validation job run"),
            "passedValidation": ContainerProperty(
                type=Boolean(), description="True if validation passed (no violations), False otherwise"
            ),
            "resultSeverity": ContainerProperty(
                type=Text(is_list=True),
                description="List of severities from violations: Violation, Warning, Info",
            ),
            "failedConstraints": ContainerProperty(type=Text(is_list=True), description="List of failed constraints"),
            "focusNode": ContainerProperty(type=Text(), description="URI of the focus node that was validated"),
            "focusNodeInstance": ContainerProperty(
                type=DirectRelation(), description="Reference to the validated instance"
            ),
            "sourceShape": ContainerProperty(type=Text(is_list=True), description="List of source shape URIs"),
            "targetClass": ContainerProperty(
                type=Text(is_list=True),
                description=(
                    "SHACL targetClass local names (e.g. EquipmentType, Document)"
                    " — useful for filtering and aggregation"
                ),
            ),
            "dimensions": ContainerProperty(
                type=Text(is_list=True),
                description="Data quality dimensions violated (Completeness, Validity, Conformity, Consistency)",
            ),
            "dataDomainExternalId": ContainerProperty(type=Text(), description="External ID of the data domain"),
            "validationReport": ContainerProperty(type=Json(), description="Full validation report with violations"),
        }

        container = ContainerApply(
            space=space,
            external_id=container_id,
            properties=properties,
            description="Data quality validation records from SHACL validation",
            used_for="record",
        )
        client.data_modeling.containers.apply(container)
    except CogniteAPIError as e:
        if e.code == 403:
            raise RuntimeError(
                f"Container {space}.{container_id} does not exist and you don't have permission to create it. "
                f"Please ask an administrator to create the container first."
            ) from e
        raise
