from __future__ import annotations

import time
from pathlib import Path
from typing import Literal

from cognite.client import CogniteClient

from cognite_data_quality._config import DataModelConfig, RuleConfig, ValidationResult
from cognite_data_quality._records import RecordsConfig, build_records, ensure_records_infrastructure
from cognite_data_quality._rules import extract_target_views_from_shacl, load_rules
from cognite_data_quality._validation import validate_instances


def _resolve_rule_config(
    rule_config: RuleConfig,
    *,
    datamodel: DataModelConfig | None,
    instance_space: str | None,
    auto_load_depth: int | None,
    verbose: bool | None,
    records_config: RecordsConfig | None,
) -> RuleConfig:
    resolved = RuleConfig(
        datamodel=datamodel or rule_config.datamodel,
        instance_space=instance_space or rule_config.instance_space,
        auto_load_depth=auto_load_depth or rule_config.auto_load_depth,
        verbose=verbose if verbose is not None else rule_config.verbose,
        records=records_config or rule_config.records,
    )
    return resolved


def _validate_raw_table(
    *,
    client: CogniteClient,
    db_name: str,
    table_name: str,
    rules_path: Path,
    namespace_base: str,
    verbose: bool,
    limit: int | None,
    print_output: bool,
    records_config: RecordsConfig | None,
    job_run_id: str,
) -> ValidationResult:
    """Validate RAW table and optionally build Records API records."""
    from rdflib import Graph, Namespace
    from rdflib.namespace import RDF

    from cognite_data_quality._neat import NeatSession
    from cognite_data_quality._records import Violation

    neat = NeatSession(client)

    # Read SHACL rules from file
    shacl_rules = rules_path.read_text()

    # Fetch RAW rows first (needed for building records)
    if verbose:
        print(f"Fetching RAW table rows: {db_name}.{table_name}")

    raw_rows = list(client.raw.rows.list(db_name=db_name, table_name=table_name, limit=limit))
    row_count = len(raw_rows)

    # Validate RAW table
    if verbose:
        print(f"Validating {row_count} rows from {db_name}.{table_name}")

    result = neat.validate_instances.with_shacl_raw(
        db_name=db_name,
        table_name=table_name,
        shacl_rules=shacl_rules,
        limit=limit,
        verbose=print_output,
    )

    # Extract violations from report graph
    violations: list[Violation] = []
    report_graph = result.report_graph
    if report_graph and isinstance(report_graph, Graph):
        sh = Namespace("http://www.w3.org/ns/shacl#")
        for res in report_graph.subjects(predicate=RDF.type, object=sh.ValidationResult):
            violation_data: dict[str, str] = {}
            for pred, obj in report_graph.predicate_objects(subject=res):
                pred_name = str(pred).split("#")[-1]
                violation_data[pred_name] = str(obj)
            violations.append(Violation(**violation_data))

    # Build Records API records if config provided
    records = []
    if records_config:
        # Format RAW rows as instances for record building
        # For RAW: space is empty, externalId is row key
        instances = [{"space": "", "externalId": row.key} for row in raw_rows]

        records = build_records(
            instances=instances,
            violations=violations,
            job_run_id=job_run_id,
            records_config=records_config,
            namespace_base=namespace_base,
        )

    # Build ValidationResult
    validation_result = ValidationResult(
        conforms=result.conforms,
        instance_count=row_count,
        violations=violations,
        records=records,
    )

    if print_output and violations:
        print(f"\nFound {len(violations)} violation(s):")
        for i, v in enumerate(violations[:10], 1):  # Show first 10
            print(f"  {i}. {v.focusNode}: {v.resultMessage}")
        if len(violations) > 10:
            print(f"  ... and {len(violations) - 10} more")

    return validation_result


def run_validation(
    *,
    client: CogniteClient,
    rules_path: Path | str,
    rules_format: Literal["ttl", "json", "yaml"] | None = None,
    datamodel: DataModelConfig | None = None,
    instance_space: str | None = None,
    db_name: str | None = None,
    table_name: str | None = None,
    verbose: bool | None = True,
    limit: int | None = None,
    print_output: bool = True,
    records_config: RecordsConfig | None = None,
    post_to_records: bool = False,
    namespace_base: str = "http://purl.org/cognite/",
) -> ValidationResult:
    """Run SHACL validation by reading data directly from CDF.

    Supports both DMS instance validation and RAW table validation:
    - For DMS: provide datamodel and instance_space
    - For RAW: provide db_name and table_name

    Args:
        client: CogniteClient instance
        rules_path: Path to SHACL rules file (.ttl, .json, or .yaml)
        rules_format: Format of rules file (auto-detected if None)
        datamodel: DMS data model config (for DMS validation)
        instance_space: DMS instance space (for DMS validation)
        db_name: RAW database name (for RAW validation)
        table_name: RAW table name (for RAW validation)
        verbose: Print validation details
        limit: Maximum number of rows/instances to validate (None = all)
        print_output: Print per-instance/row validation output
        records_config: Records API configuration
        post_to_records: Post results to Records API
        namespace_base: Base namespace for URIs

    Returns:
        ValidationResult with conforms status and violations

    Raises:
        ValueError: If neither DMS nor RAW parameters are provided, or both are provided

    Examples:
        DMS validation:
        >>> result = run_validation(
        ...     client=client,
        ...     rules_path="rules.ttl",
        ...     datamodel=DataModelConfig(space="sp", external_id="dm", version="v1"),
        ...     instance_space="my_space"
        ... )

        RAW validation:
        >>> result = run_validation(
        ...     client=client,
        ...     rules_path="rules.ttl",
        ...     db_name="abb",
        ...     table_name="Example Motor List",
        ...     namespace_base="http://purl.org/cognite/raw/abb/Example%20Motor%20List/"
        ... )
    """
    # Detect validation mode
    is_raw_mode = db_name is not None or table_name is not None
    is_dms_mode = datamodel is not None or instance_space is not None

    if is_raw_mode and is_dms_mode:
        raise ValueError(
            "Cannot use both DMS (datamodel/instance_space) and RAW (db_name/table_name) parameters. "
            "Choose one validation mode."
        )

    if not is_raw_mode and not is_dms_mode:
        raise ValueError(
            "Must provide either DMS parameters (datamodel, instance_space) or RAW parameters (db_name, table_name)"
        )

    # Generate job run ID for Records API
    job_run_id = f"run_{int(time.time() * 1000)}"

    # RAW table validation
    if is_raw_mode:
        if db_name is None or table_name is None:
            raise ValueError("Both db_name and table_name are required for RAW validation")

        result = _validate_raw_table(
            client=client,
            db_name=db_name,
            table_name=table_name,
            rules_path=Path(rules_path),
            namespace_base=namespace_base,
            verbose=verbose or False,
            limit=limit,
            print_output=print_output,
            records_config=records_config,
            job_run_id=job_run_id,
        )

    # DMS instance validation
    else:
        shacl_rules, rule_config = load_rules(
            rules_path,
            format=rules_format,
            client=client,
            shacl_base_dir=Path(rules_path).parent,
        )
        resolved = _resolve_rule_config(
            rule_config,
            datamodel=datamodel,
            instance_space=instance_space,
            auto_load_depth=None,
            verbose=verbose,
            records_config=records_config,
        )

        if resolved.datamodel is None:
            raise ValueError("datamodel must be provided or resolved from YAML config")

        if not resolved.instance_space:
            raise ValueError("instance_space must be provided or set in rule config (YAML)")

        rules_path_obj = Path(rules_path)
        view_external_ids = None
        if rules_path_obj.suffix.lower() == ".ttl" and rules_path_obj.exists():
            target_views = extract_target_views_from_shacl(rules_path_obj)
            if target_views:
                view_external_ids = sorted(target_views)

        result = validate_instances(
            client=client,
            shacl_rules=shacl_rules,
            datamodel=resolved.datamodel,
            instance_space=resolved.instance_space,
            view_external_ids=view_external_ids,
            verbose=resolved.verbose,
            limit=limit,
            print_validation_output=print_output,
            records_config=resolved.records,
            namespace_base=namespace_base,
        )

    if not print_output:
        entity_type = "rows" if is_raw_mode else "instances"
        count = len(result.violations) if is_raw_mode else result.instance_count
        print(
            f"Validation complete. Conforms: {result.conforms}, "
            f"{entity_type.capitalize()}: {count}, Violations: {len(result.violations)}"
        )

    if post_to_records:
        # Get the appropriate records config
        config_to_use = records_config if is_raw_mode else resolved.records

        if config_to_use is None:
            raise ValueError("records_config must be provided when post_to_records=True")

        ensure_records_infrastructure(client, config_to_use)
        _post_records(client, result, config_to_use)

    return result


def _post_records(client: CogniteClient, result: ValidationResult, records_config: RecordsConfig | None) -> None:
    """Post validation records using shared batch posting implementation with retry logic."""
    if records_config is None or not records_config.stream_id:
        raise ValueError("records_config.stream_id is required when post_to_records=True")

    total = len(result.records)
    if total == 0:
        return

    print(f"Posting {total} validation record(s) to Records API...")
    start = time.perf_counter()

    # Use shared batch posting implementation with retry logic
    from cognite_data_quality._function_code.common.records_api import post_records_batch

    posted_count, errors = post_records_batch(
        client=client,
        records=result.records,
        stream_id=records_config.stream_id,
        batch_size=1000,
        max_retries=3,
    )

    elapsed = time.perf_counter() - start
    rate = posted_count / elapsed if elapsed > 0 else 0

    print(
        f"\nPosted {posted_count}/{total} records to stream {records_config.stream_id}"
        f" in {elapsed:.1f}s ({rate:.0f} records/sec)."
    )

    if errors:
        print(f"⚠️  {len(errors)} record(s) failed to post:")
        for err in errors[:5]:
            print(f"  - {err['record']}: {err['error']}")
        if len(errors) > 5:
            print(f"  ... and {len(errors) - 5} more errors")
