"""Template configuration generator for cognite_data_quality.

Generates starter configuration files (settings.yaml, view configs, SHACL rules)
from CDF data models to simplify initial setup.
"""

from __future__ import annotations

import re
from pathlib import Path
from typing import Any

import yaml
from cognite.client import CogniteClient


def generate_template(
    client: CogniteClient,
    output_dir: Path | str,
    environment_name: str = "my-environment",
    data_model_ids: list[tuple[str, str, str]] | None = None,
    instance_space: str | None = None,
    interactive: bool = True,
    include_timeseries: bool = False,
    overwrite: bool = False,
) -> dict[str, Any]:
    """Generate template configuration folders for cognite_data_quality.

    Args:
        client: CogniteClient for querying data models
        output_dir: Base output directory
        environment_name: Environment name (e.g., 'prod', 'dev')
        data_model_ids: List of (space, external_id, version) tuples.
                       If None and interactive=True, prompts user to select.
                       If None and interactive=False, raises ValueError.
        instance_space: Default instance space for validation
        interactive: If True, prompts for data model selection if data_model_ids is None
        include_timeseries: If True, generates sample timeseries config
        overwrite: If True, overwrites existing files

    Returns:
        dict with generation summary:
        {
            "environment": str,
            "output_path": Path,
            "generated_files": {
                "settings": Path,
                "views": list[Path],
                "shacl_rules": list[Path],
                "timeseries": list[Path] | None
            },
            "data_models": list[dict],
            "views_count": int,
            "warnings": list[str]
        }

    Raises:
        ValueError: If data_model_ids is None and interactive=False,
                   or if environment_name is invalid
        FileExistsError: If output directory exists and overwrite=False
        RuntimeError: If data model query fails or no views found

    Example:
        >>> from cognite_data_quality import generate_template
        >>> from cognite.client import CogniteClient
        >>>
        >>> client = CogniteClient()
        >>>
        >>> # Interactive mode
        >>> result = generate_template(
        ...     client=client,
        ...     output_dir="config",
        ...     environment_name="prod",
        ...     interactive=True
        ... )
        >>>
        >>> # Non-interactive mode
        >>> result = generate_template(
        ...     client=client,
        ...     output_dir="config",
        ...     environment_name="dev",
        ...     data_model_ids=[
        ...         ("sp_enterprise_process_industry", "Equipment", "v1"),
        ...     ],
        ...     instance_space="my_data_space",
        ...     interactive=False
        ... )
    """
    # Validate environment name
    if not re.match(r"^[a-zA-Z0-9_-]+$", environment_name):
        raise ValueError(
            f"Invalid environment name: '{environment_name}'. "
            "Must contain only letters, numbers, hyphens, and underscores."
        )

    # Query or select data models
    if data_model_ids is None:
        if not interactive:
            raise ValueError("data_model_ids is required when interactive=False")
        data_model_ids = _interactive_select_data_models(client)

    # Query CDF for data models
    print("\nQuerying CDF for data models...")
    view_infos = _query_data_models(client, data_model_ids)

    if not view_infos:
        raise RuntimeError("No views found in selected data models")

    # Prepare output directory
    output_path = Path(output_dir) / "config" / "environments" / environment_name
    if output_path.exists() and not overwrite:
        raise FileExistsError(
            f"Output directory already exists: {output_path}\nUse overwrite=True to replace existing files."
        )

    print(f"\nGenerating configuration for {len(view_infos)} view(s)...")
    print(f"Output directory: {output_path}")

    # Create directory structure
    output_path.mkdir(parents=True, exist_ok=True)
    (output_path / "views").mkdir(exist_ok=True)
    (output_path / "shacl_rules").mkdir(exist_ok=True)
    if include_timeseries:
        (output_path / "timeseries").mkdir(exist_ok=True)

    # Generate files
    generated_files: dict[str, Any] = {
        "settings": None,
        "views": [],
        "shacl_rules": [],
        "timeseries": None if not include_timeseries else [],
    }
    warnings: list[str] = []

    # Generate settings.yaml
    print("\n  Generating settings.yaml...")
    settings_content = _generate_settings_yaml(environment_name, include_timeseries)
    settings_path = output_path / "settings.yaml"
    _write_yaml_file(settings_path, settings_content)
    generated_files["settings"] = settings_path

    # Generate view configs and SHACL rules
    for view_info in view_infos:
        view = view_info["view"]
        print(f"  Generating config for {view.external_id}...")

        # Generate view config
        config_filename, config_content = _generate_view_config(view_info, instance_space, environment_name)
        config_path = output_path / "views" / config_filename
        _write_yaml_file(config_path, config_content)
        generated_files["views"].append(config_path)

        # Generate SHACL rules
        shacl_content = _generate_minimal_shacl_rules(view_info)
        shacl_filename = f"{view.external_id.lower()}_shacl_rules.ttl"
        shacl_path = output_path / "shacl_rules" / shacl_filename
        _write_ttl_file(shacl_path, shacl_content)
        generated_files["shacl_rules"].append(shacl_path)

    # Generate timeseries config if requested
    if include_timeseries:
        print("  Generating timeseries config example...")
        ts_content = _generate_timeseries_config(environment_name)
        ts_path = output_path / "timeseries" / "example_timeseries_quality.yaml"
        _write_yaml_file(ts_path, ts_content)
        generated_files["timeseries"] = [ts_path]

        # Generate timeseries SHACL rules
        ts_shacl_content = _generate_timeseries_shacl_rules()
        ts_shacl_path = output_path / "shacl_rules" / "timeseries_quality_rules.ttl"
        _write_ttl_file(ts_shacl_path, ts_shacl_content)
        generated_files["shacl_rules"].append(ts_shacl_path)

    print("\n✓ Successfully generated configuration!")
    print("\nNext steps:")
    print("1. Review and customize the generated SHACL rules in:")
    print(f"   {output_path / 'shacl_rules'}")
    print("2. Update instance_space in view configs if needed:")
    print(f"   {output_path / 'views'}")
    print("3. Deploy with: deploy_validation_infrastructure(")
    print("       client=client,")
    print(f"       settings_path='{output_path / 'settings.yaml'}'")
    print("   )")

    return {
        "environment": environment_name,
        "output_path": output_path,
        "generated_files": generated_files,
        "data_models": [
            {"space": vi["data_model"].space, "external_id": vi["data_model"].external_id} for vi in view_infos
        ],
        "views_count": len(view_infos),
        "warnings": warnings,
    }


def _interactive_select_data_models(client: CogniteClient) -> list[tuple[str, str, str]]:
    """Prompt user to select data models using basic input()."""
    print("\nQuerying CDF for available data models...")

    try:
        data_models_list = client.data_modeling.data_models.list(limit=-1)
    except Exception as e:
        raise RuntimeError(f"Failed to list data models from CDF: {e}") from e

    # Filter to models with views
    data_models_with_views = [dm for dm in data_models_list if dm.views and len(dm.views) > 0]

    if not data_models_with_views:
        raise RuntimeError("No data models with views found in CDF")

    print(f"\nFound {len(data_models_with_views)} data model(s) with views:\n")

    # Display numbered list
    for idx, dm in enumerate(data_models_with_views, 1):
        view_count = len(dm.views) if dm.views else 0
        print(f"  [{idx}] {dm.space}/{dm.external_id}:{dm.version} ({view_count} views)")

    # Get user input
    print("\nEnter numbers to select (comma-separated, or 'all'):")
    user_input = input("> ").strip()

    if user_input.lower() == "all":
        selected_indices = list(range(len(data_models_with_views)))
    else:
        try:
            selected_indices = [int(x.strip()) - 1 for x in user_input.split(",")]
        except ValueError:
            raise ValueError("Invalid input. Please enter numbers separated by commas, or 'all'") from None

    # Validate and extract selected data models
    data_model_ids = []
    for idx in selected_indices:
        if 0 <= idx < len(data_models_with_views):
            dm = data_models_with_views[idx]
            data_model_ids.append((dm.space, dm.external_id, dm.version))
        else:
            print(f"Warning: Ignoring invalid selection: {idx + 1}")

    if not data_model_ids:
        raise ValueError("No valid data models selected")

    print(f"\nSelected {len(data_model_ids)} data model(s)")
    return data_model_ids


def _query_data_models(client: CogniteClient, data_model_ids: list[tuple[str, str, str]]) -> list[dict[str, Any]]:
    """Query CDF for data models and extract view information.

    Returns list of dicts with keys: 'data_model', 'view', 'properties'
    """
    try:
        # Convert tuples to proper DataModelId format for API
        from cognite.client.data_classes.data_modeling import DataModelId

        dm_ids = [DataModelId(space=s, external_id=e, version=v) for s, e, v in data_model_ids]

        data_models = client.data_modeling.data_models.retrieve(dm_ids, inline_views=True)
    except Exception as e:
        raise RuntimeError(f"Failed to retrieve data models from CDF: {e}") from e

    if not data_models:
        raise ValueError(f"No data models found for IDs: {data_model_ids}")

    # Extract view information
    view_infos = []
    for data_model in data_models:
        if not data_model.views:
            continue

        for view in data_model.views:
            # Skip edge-only views (only include node or all views)
            used_for = getattr(view, "used_for", None)
            if used_for not in ("node", "all", None):
                continue

            # Extract properties
            properties = {}
            if hasattr(view, "properties") and view.properties:
                for prop_name, prop_def in view.properties.items():
                    properties[prop_name] = {
                        "name": prop_name,
                        "type": getattr(prop_def, "type", None),
                        "nullable": getattr(prop_def, "nullable", True),
                        "description": getattr(prop_def, "description", ""),
                    }

            view_infos.append({"data_model": data_model, "view": view, "properties": properties})

    if not view_infos:
        raise RuntimeError("No node views found in selected data models")

    return view_infos


def _generate_settings_yaml(env_name: str, include_timeseries: bool) -> dict[str, Any]:
    """Generate settings.yaml with sensible defaults."""
    settings: dict[str, Any] = {
        "workflow": {
            "external_id_prefix": f"dq-{env_name}",
            "version": "1",
            "task": {"retries": 3, "timeout": 600, "on_failure": "abortWorkflow"},
        },
        "trigger": {
            "external_id_prefix": f"dq-{env_name}-trigger",
            "batch_size": 10,
            "batch_timeout": 60,
        },
        "records": {
            "space": "dataQuality",
            "container": "DataQualityValidationRecord",
            "stream_id": f"{env_name}_validation_stream",
        },
        "shacl_rules": {
            "source_dir": "shacl_rules",
            "mime_type": "text/turtle",
            "dataset_external_id": f"{env_name}_shacl_rules",
        },
    }

    if include_timeseries:
        settings["timeseries_workflow"] = {
            "external_id_prefix": f"dq-ts-{env_name}",
            "version": "1",
            "task": {"retries": 3, "timeout": 1800, "on_failure": "abortWorkflow"},
        }
        settings["timeseries"] = {"config_dir": "timeseries"}

    return settings


def _generate_view_config(
    view_info: dict[str, Any], instance_space: str | None, env_name: str
) -> tuple[str, dict[str, Any]]:
    """Generate view config YAML. Returns (filename, config_dict)."""
    view = view_info["view"]
    data_model = view_info["data_model"]

    # Create filename from view external_id
    filename = f"{view.external_id.lower()}.yaml"

    config: dict[str, Any] = {
        "view": {"space": view.space, "external_id": view.external_id, "version": view.version},
        "instance_space": instance_space or view.space,
        "shacl_rules": {
            "file": f"{view.external_id.lower()}_shacl_rules.ttl",
            "external_id": f"{view.external_id.lower()}_shacl_rules",
        },
        "datamodel": {
            "space": data_model.space,
            "external_id": data_model.external_id,
            "version": data_model.version,
        },
        "validation": {"auto_load_depth": 2, "verbose": True},
        "partition_count": 10,
        "partition_field": "lastUpdatedTime",
        "records": {
            "rule_set_id": f"{view.external_id}SHACLv1",
            "rule_set_version": "1.0",
        },
    }

    return filename, config


def _generate_minimal_shacl_rules(view_info: dict[str, Any]) -> str:
    """Generate minimal SHACL rules with TODO comments."""
    view = view_info["view"]
    properties = view_info.get("properties", {})

    # Select a few sample properties
    sample_props = _select_sample_properties(properties)

    ttl = f"""@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .

# Namespace for the data model
@prefix dm: <http://purl.org/cognite/{view.space}/{view.external_id}/> .

# =============================================================================
# SHACL Rules for {view.external_id}
# =============================================================================
# This is a starter template. Customize the rules below for your validation needs.
#
# Common SHACL constraints:
#   sh:minCount, sh:maxCount - Cardinality constraints
#   sh:datatype - Data type validation (xsd:string, xsd:integer, xsd:dateTime, etc.)
#   sh:pattern - Regular expression matching
#   sh:minLength, sh:maxLength - String length constraints
#   sh:minInclusive, sh:maxInclusive - Numeric range constraints
#   sh:node - Reference to another shape (triggers auto-loading)
#   sh:sparql - Custom SPARQL constraint for complex logic
#
# Severity levels: sh:Violation (error), sh:Warning, sh:Info
# =============================================================================

dm:{view.external_id}Shape
    a sh:NodeShape ;
    sh:targetClass dm:{view.external_id} ;
    sh:name "{view.external_id} Validation" ;
    sh:description "Validates {view.external_id} instances" ;

"""

    # Add sample property constraints
    if sample_props:
        ttl += "    # Property validation rules (customize these for your needs)\n"
        for prop_name, prop_info in sample_props.items():
            ttl += _generate_property_constraint(prop_name, prop_info, view.external_id)
    else:
        ttl += """    # TODO: Add your validation rules here
    # Example - require a 'name' property:
    # sh:property [
    #     sh:path dm:name ;
    #     sh:minCount 1 ;
    #     sh:datatype xsd:string ;
    #     sh:message "{view.external_id} must have a name" ;
    #     sh:severity sh:Violation ;
    # ] ;
"""

    ttl += "\n    .\n"
    return ttl


def _select_sample_properties(properties: dict[str, Any], max_props: int = 3) -> dict[str, Any]:
    """Select 2-3 sample properties for starter rules."""
    # Priority: name > description > title > other string fields
    priority = ["name", "description", "title", "external_id"]
    selected = {}

    for prop_name in priority:
        if prop_name in properties:
            selected[prop_name] = properties[prop_name]
            if len(selected) >= max_props:
                break

    # If still under max, add any other properties
    if len(selected) < max_props:
        for prop_name, prop_info in properties.items():
            if prop_name not in selected:
                selected[prop_name] = prop_info
                if len(selected) >= max_props:
                    break

    return selected


def _generate_property_constraint(prop_name: str, prop_info: dict[str, Any], view_name: str) -> str:
    """Generate a SHACL property constraint based on property metadata."""
    constraint = f"""    sh:property [
        sh:path dm:{prop_name} ;
        # TODO: Customize constraint for {prop_name}
"""

    # Add sensible defaults based on property type
    if not prop_info.get("nullable", True):
        constraint += "        sh:minCount 1 ;\n"

    prop_type = prop_info.get("type")
    if isinstance(prop_type, dict):
        type_name = prop_type.get("type", "")
        if type_name == "text":
            constraint += "        sh:datatype xsd:string ;\n"
        elif type_name in ("int32", "int64"):
            constraint += "        sh:datatype xsd:integer ;\n"
        elif type_name == "float":
            constraint += "        sh:datatype xsd:float ;\n"
        elif type_name == "boolean":
            constraint += "        sh:datatype xsd:boolean ;\n"
        elif type_name == "timestamp":
            constraint += "        sh:datatype xsd:dateTime ;\n"

    description = prop_info.get("description", "")
    if description:
        # Escape quotes in description
        description = description.replace('"', '\\"')
        constraint += f'        sh:message "{view_name} {prop_name}: {description}" ;\n'
    else:
        constraint += f'        sh:message "{view_name} must have valid {prop_name}" ;\n'

    constraint += "        sh:severity sh:Warning ;\n"
    constraint += "    ] ;\n"

    return constraint


def _generate_timeseries_config(env_name: str) -> dict[str, Any]:
    """Generate example timeseries validation config."""
    return {
        "name": "example_timeseries_quality",
        "description": "Example time series quality validation",
        "schedule": {"cron": "0 * * * *"},  # Every hour
        "filter": {"space": "TODO_timeseries_space", "external_id_prefix": "TODO_prefix_"},
        "datamodel": {"space": "cdf_cdm", "external_id": "CogniteTimeSeries", "version": "v1"},
        "shacl_rules": {"file": "timeseries_quality_rules.ttl", "external_id": "timeseries_quality_rules"},
        "validation": {"verbose": True},
        "records": {"rule_set_id": "TimeSeriesSHACLv1", "rule_set_version": "1.0"},
    }


def _generate_timeseries_shacl_rules() -> str:
    """Generate example timeseries SHACL rules with CDF/INDSL functions."""
    return """@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix cdf: <http://purl.org/cognite/cdf/> .
@prefix indsl: <http://purl.org/cognite/indsl/> .
@prefix ts: <http://purl.org/cognite/cdf_cdm/CogniteTimeSeries/> .

# =============================================================================
# Time Series Quality Validation Rules (Example)
# =============================================================================
# These rules use CDF SDK and INDSL functions to validate time series data quality.
#
# Available functions:
#   cdf:datapoints_count(time_window) - Count datapoints in time window
#   indsl:outliers_detect(time_window, threshold) - Detect outliers
#   indsl:gaps_detect(time_window, max_gap) - Detect gaps in data
#
# Time windows: "1h-ago:now", "24h-ago:now", "7d-ago:now", etc.
# =============================================================================

ts:TimeSeriesQualityShape
    a sh:NodeShape ;
    sh:targetClass ts:CogniteTimeSeries ;
    sh:name "Time Series Data Quality" ;
    sh:description "Validates time series data availability and quality" ;

    # TODO: Add your time series validation rules here
    # Example - check for minimum datapoints in last hour:
    # sh:sparql [
    #     a sh:SPARQLConstraint ;
    #     sh:message "Time series has insufficient data points (expected >= 60, got {?count})" ;
    #     sh:severity sh:Warning ;
    #     sh:select \"\"\"
    #         PREFIX cdf: <http://purl.org/cognite/cdf/>
    #         SELECT $this ?count
    #         WHERE {
    #             BIND(cdf:datapoints_count("1h-ago:now") AS ?count)
    #             FILTER(?count < 60)
    #         }
    #     \"\"\" ;
    # ] .

    .
"""


def _write_yaml_file(path: Path, content: dict[str, Any]) -> None:
    """Write YAML file with nice formatting."""
    path.parent.mkdir(parents=True, exist_ok=True)
    with path.open("w", encoding="utf-8") as f:
        yaml.dump(content, f, default_flow_style=False, sort_keys=False, allow_unicode=True)


def _write_ttl_file(path: Path, content: str) -> None:
    """Write TTL file."""
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(content, encoding="utf-8")


def _analyze_raw_schema(rows: list) -> tuple[dict, int]:
    """
    Analyze RAW rows to discover column schema.

    Args:
        rows: List of Row objects from CDF RAW API

    Returns:
        Tuple of (schema_dict, row_count) where schema_dict contains:
        {
            column_name: {
                "type": "string" | "int" | "float" | "boolean",
                "present_in": int,  # Number of rows with this column
                "nullable": bool,   # True if any row has None/null value
            }
        }
    """
    from collections import defaultdict

    column_stats: dict[str, dict[str, Any]] = defaultdict(
        lambda: {
            "types": set(),
            "present_in": 0,
            "nullable": False,
        }
    )

    for row in rows:
        columns = row.columns if hasattr(row, "columns") else row
        for col_name, value in columns.items():
            stats = column_stats[col_name]
            stats["present_in"] += 1

            if value is None:
                stats["nullable"] = True
            else:
                # Infer type
                if isinstance(value, bool):
                    stats["types"].add("boolean")
                elif isinstance(value, int):
                    stats["types"].add("int")
                elif isinstance(value, float):
                    stats["types"].add("float")
                else:
                    stats["types"].add("string")

    # Consolidate types (prefer most specific)
    schema = {}
    for col_name, stats in column_stats.items():
        # Choose most restrictive type if multiple found
        types = stats["types"]
        if "string" in types:
            col_type = "string"  # Fallback to string if mixed with other types
        elif "float" in types:
            col_type = "float"
        elif "int" in types:
            col_type = "int"
        elif "boolean" in types:
            col_type = "boolean"
        else:
            col_type = "string"

        schema[col_name] = {
            "type": col_type,
            "present_in": stats["present_in"],
            "nullable": stats["nullable"],
        }

    return schema, len(rows)


def _generate_raw_shacl_from_schema(
    db_name: str,
    table_name: str,
    schema: dict,
    row_count: int,
    required_columns: list[str] | None = None,
) -> str:
    """
    Generate SHACL rules from discovered RAW table schema.

    Creates rules for:
    - Required columns (sh:minCount 1)
    - Column types (sh:datatype)
    - Presence statistics (as comments)

    Args:
        db_name: RAW database name
        table_name: RAW table name
        schema: Column schema from _analyze_raw_schema()
        row_count: Number of rows sampled during schema analysis
        required_columns: List of column names that must be present

    Returns:
        SHACL rules as Turtle string
    """
    # Generate namespace-safe URIs
    namespace = f"raw_{db_name}_{table_name}"
    uri_base = f"http://purl.org/cognite/raw/{db_name}/{table_name}/"

    # Build SHACL document
    rules = [
        "@prefix sh: <http://www.w3.org/ns/shacl#> .",
        "@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .",
        f"@prefix {namespace}: <{uri_base}> .",
        "",
        f"# SHACL template for RAW table: {db_name}.{table_name}",
        f"# Auto-generated from {len(schema)} columns found in sample rows",
        "# Edit this template to add custom constraints (ranges, patterns, etc.)",
        "",
    ]

    # Create shape for each column
    for col_name, col_schema in schema.items():
        is_required = required_columns and col_name in required_columns
        # BUG FIX: Divide by row_count instead of len(schema)
        col_presence = (col_schema["present_in"] / row_count) * 100 if row_count > 0 else 0

        # Column presence shape
        shape_name = f"{namespace}:{col_name.replace('-', '_').replace(' ', '_')}Shape"
        rules.extend(
            [
                f"# Column: {col_name}",
                f"# Type: {col_schema['type']}, Present in: {col_presence:.0f}% of rows,"
                f" Nullable: {col_schema['nullable']}",
                f"{shape_name}",
                "    a sh:NodeShape ;",
                f"    sh:targetClass {namespace}:{table_name} ;",
                "    sh:property [",
                f"        sh:path {namespace}:{col_name.replace('-', '_').replace(' ', '_')} ;",
            ]
        )

        # Add minCount if required
        if is_required:
            rules.append("        sh:minCount 1 ;")

        # Add datatype constraint
        xsd_type = {
            "string": "xsd:string",
            "int": "xsd:integer",
            "float": "xsd:double",
            "boolean": "xsd:boolean",
        }.get(col_schema["type"], "xsd:string")

        if not col_schema["nullable"]:
            rules.append(f"        sh:datatype {xsd_type} ;")

        # Add single message (prefer required message over type message)
        if is_required:
            rules.append(f'        sh:message "{col_name} is required" ;')
        elif not col_schema["nullable"]:
            rules.append(f'        sh:message "{col_name} must be of type {col_schema["type"]}" ;')

        rules.extend(["    ] .", ""])

    return "\n".join(rules)


def generate_raw_table_template(
    client: CogniteClient,
    output_dir: Path | str,
    environment_name: str,
    db_name: str,
    table_name: str,
    sample_size: int = 1000,
    required_columns: list[str] | None = None,
    overwrite: bool = False,
    verbose: bool = True,
) -> dict[str, Any]:
    """Generate template configuration for RAW table validation.

    Creates:
    - tables/{db_name}_{table_name}.yaml (table config)
    - shacl_rules/{db_name}_{table_name}_shacl_rules.ttl (SHACL rules generated from table schema)

    Args:
        client: CogniteClient for accessing RAW table
        output_dir: Output directory (typically the environment directory like config/environments/cog-sail)
        environment_name: Environment name (e.g., 'cog-sail', 'prod') - used for metadata only
        db_name: RAW database name
        table_name: RAW table name
        sample_size: Number of rows to analyze for SHACL template (default: 1000)
        required_columns: Columns that must be present (if None, all are optional)
        overwrite: If True, overwrites existing files
        verbose: Print progress messages

    Returns:
        dict with generation summary:
        {
            "environment": str,
            "output_path": Path,
            "table_config": Path,
            "shacl_rules": Path,
            "columns_found": int,
            "rows_analyzed": int
        }

    Raises:
        FileExistsError: If output files exist and overwrite=False
        RuntimeError: If table has no rows or SHACL generation fails

    Example:
        >>> from cognite_data_quality import generate_raw_table_template
        >>> from cognite.client import CogniteClient
        >>>
        >>> client = CogniteClient()
        >>>
        >>> # From within the environment directory (config/environments/cog-sail/)
        >>> result = generate_raw_table_template(
        ...     client=client,
        ...     output_dir=".",  # Current directory
        ...     environment_name="cog-sail",
        ...     db_name="abb",
        ...     table_name="Example Motor List",
        ...     required_columns=["motor_id", "status"],
        ... )
        >>>
        >>> # Or specify full path from anywhere
        >>> result = generate_raw_table_template(
        ...     client=client,
        ...     output_dir="config/environments/cog-sail",
        ...     environment_name="cog-sail",
        ...     db_name="abb",
        ...     table_name="Example Motor List",
        ... )
    """

    # Validate environment name
    if not re.match(r"^[a-zA-Z0-9_-]+$", environment_name):
        raise ValueError(
            f"Invalid environment name: '{environment_name}'. "
            "Must contain only letters, numbers, hyphens, and underscores."
        )

    # Prepare output paths (use output_dir directly, no automatic nesting)
    output_path = Path(output_dir)
    tables_dir = output_path / "tables"
    shacl_dir = output_path / "shacl_rules"

    # Create safe filename from db_name and table_name
    safe_db = db_name.lower().replace(" ", "_").replace("/", "_")
    safe_table = table_name.lower().replace(" ", "_").replace("/", "_")
    config_filename = f"{safe_db}_{safe_table}.yaml"
    shacl_filename = f"{safe_db}_{safe_table}_shacl_rules.ttl"

    config_path = tables_dir / config_filename
    shacl_path = shacl_dir / shacl_filename

    # Check if files exist
    if not overwrite:
        if config_path.exists():
            raise FileExistsError(f"Table config already exists: {config_path}\nUse overwrite=True to replace.")
        if shacl_path.exists():
            raise FileExistsError(f"SHACL rules already exist: {shacl_path}\nUse overwrite=True to replace.")

    if verbose:
        print(f"\nGenerating RAW table template for {db_name}.{table_name}")
        print(f"Environment: {environment_name}")

    # Create directories
    tables_dir.mkdir(parents=True, exist_ok=True)
    shacl_dir.mkdir(parents=True, exist_ok=True)

    # Generate SHACL rules from table schema
    if verbose:
        print("\nGenerating SHACL rules from table schema...")

    try:
        # Fetch and analyze RAW rows
        rows = list(
            client.raw.rows.list(
                db_name=db_name,
                table_name=table_name,
                limit=sample_size,
            )
        )

        if not rows:
            raise ValueError(f"No rows found in {db_name}.{table_name}")

        if verbose:
            print(f"  Analyzing {len(rows)} rows from {db_name}.{table_name}...")

        # Analyze schema
        schema, row_count = _analyze_raw_schema(rows)

        if verbose:
            print(f"  Found {len(schema)} columns")

        # Generate SHACL with bug fix applied
        shacl_content = _generate_raw_shacl_from_schema(
            db_name=db_name,
            table_name=table_name,
            schema=schema,
            row_count=row_count,  # BUG FIX: Pass row count instead of using len(schema)
            required_columns=required_columns,
        )
    except Exception as e:
        raise RuntimeError(f"Failed to generate SHACL template: {e}") from e

    # Write SHACL rules
    _write_ttl_file(shacl_path, shacl_content)
    if verbose:
        print(f"  ✓ Saved SHACL rules: {shacl_path}")

    # Generate table config
    namespace_base = f"http://purl.org/cognite/raw/{db_name}/{table_name}/"
    # URL encode spaces in namespace
    namespace_base = namespace_base.replace(" ", "%20")

    config_content = {
        "table": {"database": db_name, "name": table_name},
        "shacl_rules": {"file": shacl_filename, "external_id": f"{safe_db}_{safe_table}_shacl_rules"},
        "validation": {"namespace_base": namespace_base, "verbose": True},
        "partition_count": 5,
        "records": {
            "rule_set_id": f"{safe_db.upper()}{safe_table.title().replace('_', '')}SHACLv1",
            "rule_set_version": "1.0",
            "data_domain_external_id": f"{safe_db.title().replace('_', '')}{safe_table.title().replace('_', '')}",
        },
    }

    # Write table config
    _write_yaml_file(config_path, config_content)
    if verbose:
        print(f"  ✓ Saved table config: {config_path}")

    # Count columns from schema
    columns_found = len(schema)

    if verbose:
        print("\n✓ Successfully generated RAW table template!")
        print("\nNext steps:")
        print("1. Review and customize the generated SHACL rules:")
        print(f"   {shacl_path}")
        print("2. Update table config if needed:")
        print(f"   {config_path}")
        print("3. Deploy with RAW validation workflow")

    return {
        "environment": environment_name,
        "output_path": output_path,
        "table_config": config_path,
        "shacl_rules": shacl_path,
        "columns_found": columns_found,
        "rows_analyzed": row_count,
        "db_name": db_name,
        "table_name": table_name,
    }
