"""
Write validation results to Snowflake or a local directory.

Handles:
- Schema / table / view creation (``--create-schema``)
- Inserting individual validation results into ``VALIDATION.RESULTS``
- Writing results to local JSON/CSV files when ``--output-dir`` is supplied.

The database name is derived from the project folder name (e.g.
``MY_PROJECT.VALIDATION.RESULTS``).
"""

from __future__ import annotations

import json
import logging
import shutil
from pathlib import Path
from typing import Any

LOGGER = logging.getLogger(__name__)

from snowflake.snowflake_data_validation.connector.connector_base import ConnectorBase


_SCHEMA = "VALIDATION"


# ---------------------------------------------------------------------------
# DDL templates
# ---------------------------------------------------------------------------

def _ddl_statements(db: str) -> list[str]:
    """Return all DDL statements needed for the validation schema."""
    fq = f"{db}.{_SCHEMA}"
    return [
        f"CREATE DATABASE IF NOT EXISTS {db}",
        f"CREATE SCHEMA IF NOT EXISTS {fq}",
        f"CREATE FILE FORMAT IF NOT EXISTS {fq}.JSON_FORMAT TYPE = 'JSON'",
        f"CREATE STAGE IF NOT EXISTS {fq}.BASELINES COMMENT = 'Baseline JSON files'",

        # Each result is a VARIANT row; schema never changes as new fields are added.
        # Query via RESULTS_DETAIL which flattens the JSON into typed columns.
        f"""\
CREATE TABLE IF NOT EXISTS {fq}.RESULTS (
    id   NUMBER AUTOINCREMENT PRIMARY KEY,
    data VARIANT NOT NULL
)""",
        f"""\
CREATE TABLE IF NOT EXISTS {fq}.BASELINE_ROWS (
    id NUMBER AUTOINCREMENT PRIMARY KEY,
    procedure_name VARCHAR(500) NOT NULL,
    params_hash VARCHAR(8) NOT NULL,
    result_set_index NUMBER DEFAULT 0,
    row_index NUMBER NOT NULL,
    row_data VARIANT NOT NULL,
    column_types VARIANT,
    success BOOLEAN DEFAULT TRUE,
    error_message VARCHAR(2000),
    captured_at TIMESTAMP_TZ NOT NULL,
    inserted_at TIMESTAMP_TZ DEFAULT CURRENT_TIMESTAMP()
)""",
        f"""\
CREATE OR REPLACE VIEW {fq}.RESULTS_DETAIL AS
SELECT
    id,
    data:procedure::VARCHAR                    AS procedure_name,
    data:params_hash::VARCHAR                  AS params_hash,
    data:params::VARIANT                       AS parameters,
    data:status::VARCHAR                       AS status,
    data:baseline_rows::NUMBER                 AS baseline_rows,
    data:actual_rows::NUMBER                   AS actual_rows,
    data:match_type::VARCHAR                   AS match_type,
    data:match_level::VARCHAR                  AS match_level,
    data:error::VARCHAR                        AS error_message,
    data:differences::VARIANT                  AS differences,
    data:metadata::VARIANT                     AS metadata,
    data:metadata:modifies_data::BOOLEAN       AS modifies_data,
    data:metadata:affected_tables::VARIANT     AS affected_tables,
    data:metadata:source_checksum::VARCHAR     AS source_checksum,
    data:metadata:converted_checksum::VARCHAR  AS converted_checksum,
    data:executed_at::TIMESTAMP_TZ             AS executed_at
FROM {fq}.RESULTS""",
        f"""\
CREATE OR REPLACE VIEW {fq}.SUMMARY AS
SELECT
    procedure_name,
    COUNT(*) AS total_cases,
    SUM(CASE WHEN status = 'PASS' THEN 1 ELSE 0 END) AS passed,
    SUM(CASE WHEN status = 'FAIL' THEN 1 ELSE 0 END) AS failed,
    SUM(CASE WHEN status = 'ERROR' THEN 1 ELSE 0 END) AS errors,
    SUM(CASE WHEN status = 'NO_BASELINE' THEN 1 ELSE 0 END) AS no_baseline,
    ROUND(SUM(CASE WHEN status = 'PASS' THEN 1 ELSE 0 END) * 100.0 / COUNT(*), 2) AS pass_rate,
    MAX(executed_at) AS last_run
FROM {fq}.RESULTS_DETAIL
GROUP BY procedure_name
ORDER BY procedure_name""",
        f"""\
CREATE OR REPLACE VIEW {fq}.LATEST AS
SELECT *
FROM {fq}.RESULTS_DETAIL
WHERE (procedure_name, params_hash, executed_at) IN (
    SELECT procedure_name, params_hash, MAX(executed_at)
    FROM {fq}.RESULTS_DETAIL
    GROUP BY procedure_name, params_hash
)
ORDER BY procedure_name, params_hash""",
        f"""\
CREATE OR REPLACE VIEW {fq}.FAILURES AS
SELECT
    procedure_name, params_hash, parameters, status,
    baseline_rows, actual_rows, error_message, differences, executed_at
FROM {fq}.RESULTS_DETAIL
WHERE status IN ('FAIL', 'ERROR')
ORDER BY executed_at DESC, procedure_name""",
    ]


# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------

def create_validation_schema(connector: ConnectorBase, database: str) -> None:
    """Create the validation database, schema, tables, stage, and views. Idempotent."""
    for sql in _ddl_statements(database):
        # Extract the first line of the statement for a readable label in logs.
        label = sql.strip().splitlines()[0][:120]
        LOGGER.debug("Executing DDL: %s", label)
        try:
            connector.execute_statement(sql)
            LOGGER.debug("DDL succeeded: %s", label)
        except Exception as exc:
            LOGGER.warning("DDL failed: %s\n  statement: %s\n  error: %s", label, sql[:300], exc)


# Keys present in the result dict that should never be stored in Snowflake.
_PRIVATE_KEYS = ("_baseline_rows", "_actual_rows", "in_memory_diff")


def write_results_batch(
    connector: ConnectorBase,
    results: list[dict[str, Any]],
    database: str,
) -> None:
    if not results:
        return

    # INSERT INTO ... SELECT PARSE_JSON($$...$$) UNION ALL ...
    # Dollar-sign quoting avoids Snowflake's backslash-escape interpretation
    # in string literals, so json.dumps() output is passed to PARSE_JSON
    # exactly as-is (important when values contain embedded JSON with \").
    fq = f"{database}.{_SCHEMA}"
    rows = []
    for r in results:
        cleaned = {k: v for k, v in r.items() if k not in _PRIVATE_KEYS}
        data_json = json.dumps(cleaned, default=str)
        rows.append(f"SELECT PARSE_JSON($${data_json}$$) AS data")

    sql = f"INSERT INTO {fq}.RESULTS (data)\n{' UNION ALL '.join(rows)}"
    connector.execute_statement(sql)


def write_results_local(results: list[dict[str, Any]], output_dir: Path) -> None:
    """Write validation results to *output_dir* as JSON and per-case row dumps.

    Clears *output_dir* first (removes and recreates) so each run starts fresh.

    Writes:
    - ``results.json`` — full result list including ``in_memory_diff``.
    - ``{params_hash}_baseline.json`` — baseline rows for each in-memory case.
    - ``{params_hash}_actual.json``   — actual rows returned by Snowflake.

    Args:
        results: List of result dicts as returned by the validation pipeline.
        output_dir: Directory path to write output files into.
    """
    output_dir = Path(output_dir)
    if output_dir.exists():
        shutil.rmtree(output_dir)
    output_dir.mkdir(parents=True)

    # Write per-case row dumps before stripping private keys
    for r in results:
        h = r.get("params_hash", "unknown")
        for side in ("baseline", "actual"):
            key = f"_{side}_rows"
            rows = r.pop(key, None)
            if rows is not None:
                row_path = output_dir / f"{h}_{side}.json"
                row_path.write_text(
                    json.dumps(rows, indent=2, default=str), encoding="utf-8"
                )
                LOGGER.info("Wrote %d %s row(s) to %s", len(rows), side, row_path)

    json_path = output_dir / "results.json"
    json_path.write_text(json.dumps(results, indent=2, default=str), encoding="utf-8")
    LOGGER.info("Wrote %d result(s) to %s", len(results), json_path)
