"""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 .quad_store import QuadStore, _extract_step_ids
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 [
        f"https://cogniflow.org/ns#{term}",
        f"http://cogniflow.org/ns#{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(
    *,
    steps: 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 steps:
        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 steps:
            doc = json.loads(Path(path).read_text(encoding="utf-8"))
            target_graph_ids.extend(_extract_step_ids(doc))
            store.ingest_jsonld(
                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
