"""Signature generation for CogniFlow steps from DuckDB semantics."""

from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path
from typing import Iterable, List, Optional
import hashlib
import json
import logging
import tempfile

import duckdb

from .namespaces import cf_iri
from .quad_store import QuadStore, _extract_step_ids
from .rdf_document import load_rdf_document
from .semantics_paths import resolve_semantics_db_path, resolve_semantics_dir

logger = logging.getLogger(__name__)


_RDF_TYPE = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type"


def _cf_iri(term: str) -> List[str]:
    return [cf_iri(term)]


_CF_PROCESSING_STEP = _cf_iri("ProcessingStep")
_CF_HAS_PARAMETER = _cf_iri("hasParameter")
_CF_REQUIRES_PARAMETER = _cf_iri("requiresParameter")
_CF_HAS_INPUT = _cf_iri("hasInput")
_CF_HAS_OUTPUT = _cf_iri("hasOutput")
_CF_PARAMETER_NAME = _cf_iri("parameterName")
_CF_PARAM_KEY = _cf_iri("paramKey")
_CF_PORT_KEY = _cf_iri("portKey")


def _sql_placeholders(values: Iterable[object]) -> str:
    return ", ".join(["?"] * len(list(values)))


def _sha256_hex(text: str) -> str:
    return hashlib.sha256(text.encode("utf-8")).hexdigest()


def _canonical_signature_json(
    step_iri: str,
    parameter_keys: List[str],
    input_keys: List[str],
    output_keys: List[str],
) -> str:
    payload = {
        "step": step_iri,
        "parameters": parameter_keys,
        "inputs": input_keys,
        "outputs": output_keys,
    }
    return json.dumps(payload, separators=(",", ":"), ensure_ascii=False)


def _local_name(value: str) -> str:
    if "://" in value:
        pos = max(value.rfind("#"), value.rfind("/"))
        if pos >= 0:
            return value[pos + 1 :]
        return value
    if ":" in value:
        return value.split(":", 1)[1]
    return value


def _sanitize_cpp_identifier(value: str) -> str:
    base = _local_name(value)
    out = "".join(c if c.isalnum() else "_" for c in base)
    if out and out[0].isdigit():
        out = "Step_" + out
    return out or "Step"


def _unique_names(keys: List[str]) -> List[str]:
    counts: dict[str, int] = {}
    out: List[str] = []
    for key in keys:
        base = _sanitize_cpp_identifier(key)
        count = counts.get(base, 0)
        counts[base] = count + 1
        out.append(base if count == 0 else f"{base}_{count}")
    return out


@dataclass(frozen=True)
class StepSignature:
    """Compact signature inputs/outputs/parameters for one processing step."""

    graph_id: str
    step_iri: str
    parameter_keys: List[str]
    input_keys: List[str]
    output_keys: List[str]

    def signature_hash(self) -> str:
        """Return deterministic hash over canonical signature JSON payload."""
        return _sha256_hex(
            _canonical_signature_json(
                self.step_iri,
                self.parameter_keys,
                self.input_keys,
                self.output_keys,
            )
        )


def _select_first(
    con: duckdb.DuckDBPyConnection, query: str, params: List[object]
) -> Optional[str]:
    row = con.execute(query, params).fetchone()
    return str(row[0]) if row and row[0] is not None else None


def _select_objects(
    con: duckdb.DuckDBPyConnection,
    graph_id: str,
    subject: str,
    predicates: List[str],
) -> List[str]:
    if not predicates:
        return []
    placeholders = _sql_placeholders(predicates)
    rows = con.execute(
        f"""
        SELECT o
        FROM rdf_quads
        WHERE g = ? AND s = ? AND p IN ({placeholders})
        """,
        [graph_id, subject, *predicates],
    ).fetchall()
    return [str(row[0]) for row in rows if row and row[0] is not None]


def _select_object_by_predicate(
    con: duckdb.DuckDBPyConnection,
    graph_id: str,
    subject: str,
    preferred_predicates: List[str],
) -> Optional[str]:
    if not preferred_predicates:
        return None
    placeholders = _sql_placeholders(preferred_predicates)
    rows = con.execute(
        f"""
        SELECT p, o
        FROM rdf_quads
        WHERE g = ? AND s = ? AND p IN ({placeholders})
        """,
        [graph_id, subject, *preferred_predicates],
    ).fetchall()
    if not rows:
        return None
    for pred in preferred_predicates:
        for row_pred, obj in rows:
            if row_pred == pred and obj is not None:
                return str(obj)
    return None


def _step_iri_for_graph(con: duckdb.DuckDBPyConnection, graph_id: str) -> Optional[str]:
    placeholders = _sql_placeholders(_CF_PROCESSING_STEP)
    return _select_first(
        con,
        f"""
        SELECT s
        FROM rdf_quads
        WHERE g = ? AND p = ? AND o IN ({placeholders})
        LIMIT 1
        """,
        [graph_id, _RDF_TYPE, *_CF_PROCESSING_STEP],
    )


def _fetch_step_signature(
    con: duckdb.DuckDBPyConnection, graph_id: str
) -> Optional[StepSignature]:
    step_iri = _step_iri_for_graph(con, graph_id)
    if not step_iri:
        logger.debug("No ProcessingStep found in graph %s", graph_id)
        return None

    param_ids = set(_select_objects(con, graph_id, step_iri, _CF_HAS_PARAMETER))
    param_ids.update(_select_objects(con, graph_id, step_iri, _CF_REQUIRES_PARAMETER))
    input_ids = _select_objects(con, graph_id, step_iri, _CF_HAS_INPUT)
    output_ids = _select_objects(con, graph_id, step_iri, _CF_HAS_OUTPUT)

    parameter_keys: List[str] = []
    for pid in sorted(param_ids):
        key = _select_object_by_predicate(con, graph_id, pid, _CF_PARAM_KEY)
        if not key:
            key = _select_object_by_predicate(con, graph_id, pid, _CF_PARAMETER_NAME)
        if key:
            parameter_keys.append(key)
        else:
            logger.warning("Parameter %s in %s missing param key/name", pid, graph_id)

    input_keys: List[str] = []
    for iid in input_ids:
        key = _select_object_by_predicate(con, graph_id, iid, _CF_PORT_KEY)
        if key:
            input_keys.append(key)
        else:
            logger.warning("Input port %s in %s missing portKey", iid, graph_id)

    output_keys: List[str] = []
    for oid in output_ids:
        key = _select_object_by_predicate(con, graph_id, oid, _CF_PORT_KEY)
        if key:
            output_keys.append(key)
        else:
            logger.warning("Output port %s in %s missing portKey", oid, graph_id)

    return StepSignature(
        graph_id=graph_id,
        step_iri=step_iri,
        parameter_keys=sorted(parameter_keys),
        input_keys=sorted(input_keys),
        output_keys=sorted(output_keys),
    )


def load_step_signatures(
    *,
    semantics_dir: Optional[Path] = None,
    graph_ids: Optional[Iterable[str]] = None,
) -> List[StepSignature]:
    """Load step signatures from the steps DuckDB graph store."""
    base_dir = (semantics_dir or resolve_semantics_dir()).resolve()
    db_path = resolve_semantics_db_path(base_dir, kind="steps")
    if not db_path.exists():
        return []
    con = duckdb.connect(str(db_path), read_only=True)
    try:
        if graph_ids is None:
            rows = con.execute("SELECT graph_id FROM graphs ORDER BY graph_id").fetchall()
            graph_ids = [str(row[0]) for row in rows]
        signatures: List[StepSignature] = []
        for gid in graph_ids:
            sig = _fetch_step_signature(con, gid)
            if sig:
                signatures.append(sig)
        return signatures
    finally:
        con.close()


def generate_signature_header(
    *,
    documents: Optional[Iterable[Path]] = None,
    out_path: Path,
    only_prefix: Optional[str] = None,
    semantics_dir: Optional[Path] = None,
    scratch: bool = False,
) -> List[StepSignature]:
    """Generate a C++ header with stable parameter/input/output index constants."""
    temp_dir: Optional[tempfile.TemporaryDirectory[str]] = None
    base_dir: Path
    if scratch:
        temp_dir = tempfile.TemporaryDirectory(prefix="cf_siggen_")
        base_dir = Path(temp_dir.name)
    else:
        base_dir = (semantics_dir or resolve_semantics_dir()).resolve()
    base_dir.mkdir(parents=True, exist_ok=True)

    target_graph_ids: Optional[List[str]] = None
    if documents:
        target_graph_ids = []
        store = QuadStore(
            db_path=resolve_semantics_db_path(base_dir, kind="steps"),
            semantics_dir=base_dir,
        )
        store.init_schema(create_pipeline_tables=False)
        for path in documents:
            doc = load_rdf_document(Path(path), label="Step RDF document")
            target_graph_ids.extend(_extract_step_ids(doc))
            store.ingest_rdf_document(
                Path(path),
                graph_id=str(Path(path).resolve()),
                graph_type="step",
                package="custom",
                source_path=str(Path(path).resolve()),
                split_steps=True,
            )
        target_graph_ids = sorted(set(target_graph_ids))

    signatures = load_step_signatures(
        semantics_dir=base_dir,
        graph_ids=target_graph_ids,
    )

    if only_prefix:
        signatures = [sig for sig in signatures if sig.step_iri.startswith(only_prefix)]

    signatures.sort(key=lambda s: s.step_iri)

    name_counts: dict[str, int] = {}
    out_path.parent.mkdir(parents=True, exist_ok=True)
    with out_path.open("w", encoding="utf-8") as fh:
        fh.write("#pragma once\n")
        fh.write("#include <cstddef>\n\n")
        fh.write("namespace cf_generated {\n")
        step_names: dict[str, str] = {}
        for sig in signatures:
            base = _sanitize_cpp_identifier(sig.step_iri)
            count = name_counts.get(base, 0)
            name_counts[base] = count + 1
            name = base if count == 0 else f"{base}_{count}"
            step_names[sig.step_iri] = name
            fh.write(f"inline constexpr const char* k{name}Iri = \"{sig.step_iri}\";\n")
            fh.write(
                f"inline constexpr const char* k{name}SigHash = \"{sig.signature_hash()}\";\n"
            )
        fh.write("}  // namespace cf_generated\n\n")

        fh.write("namespace cf_sig {\n")
        for sig in signatures:
            name = step_names[sig.step_iri]
            param_names = _unique_names([_local_name(k) for k in sig.parameter_keys])
            input_names = _unique_names([_local_name(k) for k in sig.input_keys])
            output_names = _unique_names([_local_name(k) for k in sig.output_keys])

            fh.write(f"struct {name} {{\n")
            fh.write(f"  static constexpr size_t kParamCount = {len(param_names)};\n")
            for idx, pname in enumerate(param_names):
                fh.write(f"  static constexpr size_t P_{pname} = {idx};\n")
            fh.write(f"  static constexpr size_t kInputCount = {len(input_names)};\n")
            for idx, iname in enumerate(input_names):
                fh.write(f"  static constexpr size_t I_{iname} = {idx};\n")
            fh.write(f"  static constexpr size_t kOutputCount = {len(output_names)};\n")
            for idx, oname in enumerate(output_names):
                fh.write(f"  static constexpr size_t O_{oname} = {idx};\n")
            fh.write("};\n")
        fh.write("}  // namespace cf_sig\n")

    if temp_dir is not None:
        temp_dir.cleanup()

    return signatures
