use serde::{Deserialize, Serialize};
use uuid::Uuid;

/// Unique identifier for a dataset in the catalog.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct DatasetId(pub Uuid);

impl DatasetId {
    pub fn new() -> Self {
        Self(Uuid::new_v4())
    }
}

impl std::fmt::Display for DatasetId {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.0)
    }
}

/// Pointer to an object in storage. This is what the catalog stores — never raw data.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ObjectRef {
    pub bucket: String,
    pub key: String,
    pub size_bytes: u64,
    pub created_at: chrono::DateTime<chrono::Utc>,
}

/// Schema fingerprint — deterministic hash of an Arrow schema.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct SchemaFingerprint(pub String);

/// Sensitivity classification for a field or dataset.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Sensitivity {
    Public,      // anyone can see
    Internal,    // employees only
    Pii,         // personally identifiable (name, email, phone, SSN)
    Phi,         // protected health info (HIPAA)
    Financial,   // compensation, billing, revenue
}

/// Column-level metadata.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ColumnMeta {
    pub name: String,
    pub data_type: String,
    #[serde(default)]
    pub sensitivity: Option<Sensitivity>,
    #[serde(default)]
    pub description: String,
    #[serde(default)]
    pub is_pii: bool,
}

/// Data lineage — where this dataset came from.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Lineage {
    pub source_system: String,    // "csv_upload", "mysql_import", "api_ingest"
    pub source_file: String,      // original filename or URI
    pub ingest_job: String,       // job ID that created this dataset
    pub ingest_timestamp: chrono::DateTime<chrono::Utc>,
    #[serde(default)]
    pub parent_datasets: Vec<String>, // derived from these datasets
}

/// Freshness contract — how often this data should be updated.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FreshnessContract {
    pub expected_frequency: String, // "daily", "weekly", "hourly", "manual"
    pub stale_after_hours: u32,     // alert if not updated within this many hours
}

/// Dataset manifest — the catalog's complete view of a dataset.
/// All new fields use #[serde(default)] for backward compatibility with existing manifests.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DatasetManifest {
    pub id: DatasetId,
    pub name: String,
    pub schema_fingerprint: SchemaFingerprint,
    pub objects: Vec<ObjectRef>,
    pub created_at: chrono::DateTime<chrono::Utc>,
    pub updated_at: chrono::DateTime<chrono::Utc>,

    // --- Rich metadata (Phase 10) ---

    /// Human-readable description (auto-generated on ingest or manual)
    #[serde(default)]
    pub description: String,

    /// Who owns this dataset
    #[serde(default)]
    pub owner: String,

    /// Overall sensitivity classification
    #[serde(default)]
    pub sensitivity: Option<Sensitivity>,

    /// Column-level metadata including sensitivity and descriptions
    #[serde(default)]
    pub columns: Vec<ColumnMeta>,

    /// Where this data came from
    #[serde(default)]
    pub lineage: Option<Lineage>,

    /// How fresh the data should be
    #[serde(default)]
    pub freshness: Option<FreshnessContract>,

    /// Free-form tags for discovery
    #[serde(default)]
    pub tags: Vec<String>,

    /// Row count (updated on ingest/compact)
    #[serde(default)]
    pub row_count: Option<u64>,

    // --- Embedding freshness (Phase C) ---

    /// When the attached vector index was last refreshed. `None` means this
    /// dataset has no vector index yet, or has never been embedded.
    #[serde(default)]
    pub last_embedded_at: Option<chrono::DateTime<chrono::Utc>>,

    /// When data was written that hasn't yet been embedded. `Some(t)` means
    /// the vector index is out-of-date as of timestamp `t`. Cleared on refresh.
    #[serde(default)]
    pub embedding_stale_since: Option<chrono::DateTime<chrono::Utc>>,

    /// How this dataset wants stale embeddings handled.
    #[serde(default)]
    pub embedding_refresh_policy: Option<RefreshPolicy>,
}

/// Controls what happens when new data is written to a dataset with an
/// attached vector index.
///
/// - `Manual` (default): data writes set `embedding_stale_since`; nothing
///   embeds until an operator or agent calls `/vectors/refresh/{dataset}`.
/// - `OnAppend`: ingest fires a background refresh immediately after writing.
///   Suitable for datasets where vector freshness matters more than ingest
///   latency.
/// - `Scheduled(cron)`: a timer or external scheduler triggers refresh at
///   the named cadence. The scheduler itself is not in this ADR scope —
///   this just declares the intent so operators can see which policy a
///   dataset expects.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum RefreshPolicy {
    Manual,
    OnAppend,
    Scheduled { cron: String },
}

impl Default for RefreshPolicy {
    fn default() -> Self { Self::Manual }
}

/// AI-safe view of a dataset (Phase D).
///
/// A view is a named projection over a base dataset that:
/// - whitelists the columns visible (everything else is invisible),
/// - optionally applies a row filter (a SQL WHERE clause fragment),
/// - optionally redacts column values (e.g. mask the last 4 of an SSN).
///
/// Queries that reference the view name see only the safe projection.
/// The base table remains separately accessible by its raw name — this
/// is a *capability surface*, not a data classification fence. Use
/// access control (Phase 13) on top to enforce who can query which view.
///
/// Stored as JSON at `_catalog/views/{name}.json` in the primary bucket
/// alongside dataset manifests. The view itself doesn't store data —
/// queryd materializes it as a DataFusion view at SessionContext build
/// time.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AiView {
    pub name: String,
    /// Source dataset name. Must exist in the catalog.
    pub base_dataset: String,
    /// Whitelisted columns. Empty list is invalid (would expose nothing).
    pub columns: Vec<String>,
    /// Optional WHERE clause fragment (without the `WHERE` keyword), e.g.
    /// `status != 'blocked' AND city = 'Chicago'`. SQL injection here is
    /// the operator's problem — only trust this from authenticated admins.
    #[serde(default)]
    pub row_filter: Option<String>,
    /// Per-column redactions: `{column_name -> Redaction}`.
    #[serde(default)]
    pub column_redactions: std::collections::HashMap<String, Redaction>,
    pub created_at: chrono::DateTime<chrono::Utc>,
    #[serde(default)]
    pub created_by: String,
    #[serde(default)]
    pub description: String,
}

/// Per-model configuration for HNSW builds. Declared inline so the PRD
/// invariant 9 ("every reader gets its own profile") can carry tuning
/// preferences without forcing a round-trip to vectord's trial system.
/// Mirrors `vectord::trial::HnswConfig` to avoid a cross-crate dep cycle.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ProfileHnswConfig {
    pub ef_construction: usize,
    pub ef_search: usize,
    #[serde(default)]
    pub seed: Option<u64>,
}

impl Default for ProfileHnswConfig {
    /// Matches the production HNSW default from Phase 15 trial sweep
    /// (see docs/ADR and vectord::trial::HnswConfig::default).
    fn default() -> Self {
        Self { ef_construction: 80, ef_search: 30, seed: None }
    }
}

/// A named client of the substrate — a local LLM, an AI agent, or a
/// human role that should see a specific scoped view of the data.
/// Profiles are the first-class "every reader gets its own profile"
/// concept from PRD invariant 9.
///
/// Bound datasets can be raw catalog tables OR `AiView` names — either
/// works at query time since both register as DataFusion tables with
/// the same name. Mixing raw and views lets you compose access rules:
/// bind a model to `candidates_safe` (PII-redacted view) and `placements`
/// (raw table, no PII), and the model gets exactly what it needs.
///
/// Stored at `_catalog/profiles/{id}.json`. Activation (`/profile/{id}/
/// activate`) pre-loads the embedding cache and builds HNSW with the
/// profile's config — first search after activate is warm.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelProfile {
    /// Unique id — slug form (alphanumeric + `-`, `_`). Used in URLs,
    /// bucket names (`profile:{id}`), and trial journal prefixes.
    pub id: String,
    /// Ollama model tag (e.g. "qwen2.5:7b", "nomic-embed-text:latest").
    /// Used when this profile is the "generate" or "embed" caller.
    pub ollama_name: String,
    /// Free-text description surfaced to operators. Who/what is this
    /// profile for?
    #[serde(default)]
    pub description: String,
    /// Names of datasets or AiViews this profile can read. A search
    /// against an index whose `source` isn't in this list is refused.
    pub bound_datasets: Vec<String>,
    /// Per-profile HNSW config, used when activate builds the graph.
    #[serde(default)]
    pub hnsw_config: ProfileHnswConfig,
    /// Which embedding model this profile expects. Defaults to
    /// `nomic-embed-text` (the only one currently installed).
    #[serde(default = "default_embed_model")]
    pub embed_model: String,
    pub created_at: chrono::DateTime<chrono::Utc>,
    #[serde(default)]
    pub created_by: String,
    /// Federation layer 2: which bucket this profile's artifacts
    /// (trial journals, promotions for its indexes) live in. If set and
    /// not yet registered, `POST /vectors/profile/{id}/activate` auto-
    /// provisions a local bucket under `storage.profile_root` and
    /// registers it before warming indexes. `None` = share the primary
    /// bucket (pre-federation behavior).
    #[serde(default)]
    pub bucket: Option<String>,
    /// ADR-019 hybrid: which storage backend serves this profile's
    /// indexes. Default Parquet so pre-existing profiles unchanged.
    /// New profiles can opt into Lance for append-heavy / random-access
    /// / 5M+ vector workloads.
    #[serde(default)]
    pub vector_backend: VectorBackend,
    /// Phase 41: Profile type for routing + activation behavior.
    /// Default Execution for backward compatibility.
    #[serde(default)]
    pub profile_type: ProfileType,
}

#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum ProfileType {
    #[default]
    Execution,
    Retrieval,
    Memory,
    Observer,
}

fn default_embed_model() -> String { "nomic-embed-text".to_string() }

/// Per-profile / per-index storage backend choice.
///
/// `Parquet` (default) — vectors as binary-blob in Parquet + in-RAM HNSW
/// for ANN. Best when:
/// - Index ≤ ~5M vectors (fits in RAM)
/// - Search latency dominates (HNSW p50 < 1ms at 100K)
/// - Workload is mostly read, not append
///
/// `Lance` — vectors as native FixedSizeList in a Lance dataset, IVF_PQ
/// on-disk index. Best when:
/// - Index will exceed RAM ceiling (5M+ vectors per index)
/// - Heavy random-row access for RAG (Lance: 311us; Parquet: ~35ms full
///   scan per fetch)
/// - Append-heavy ingest (Lance: 0.08s for 10K rows; Parquet: full
///   ~330MB rewrite per change)
/// - Need to rebuild index frequently (Lance IVF_PQ: 16s; HNSW: 230s
///   at the same scale)
///
/// Defaults to Parquet for backward compat with pre-ADR-019 indexes.
/// See `docs/ADR-019-vector-storage.md` for the full scorecard.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum VectorBackend {
    Parquet,
    Lance,
}

impl Default for VectorBackend {
    fn default() -> Self { VectorBackend::Parquet }
}

/// Soft-delete marker (Phase E).
///
/// Tombstones live beside the dataset in `_catalog/tombstones/{dataset}/`
/// as append-log JSONL. Query-time filter in queryd reads all tombstones
/// for each dataset and wraps the base table in a DataFusion view that
/// excludes tombstoned rows. Physical deletion happens later (compaction),
/// so the row count immediately reflects the delete but data is still on
/// disk until compact runs. That's deliberate — it gives a reversal
/// window and keeps the event journal audit trail intact.
///
/// All tombstones for a given dataset must use the same `row_key_column`
/// (enforced on write); otherwise the query-time filter can't be built
/// as a single WHERE clause.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Tombstone {
    pub dataset: String,
    /// Column name that identifies the row (e.g. "candidate_id").
    pub row_key_column: String,
    /// Value of that column for the tombstoned row.
    pub row_key_value: String,
    pub deleted_at: chrono::DateTime<chrono::Utc>,
    /// Human / system actor responsible for the delete (audit).
    #[serde(default)]
    pub actor: String,
    /// Why (e.g. "GDPR request #1234", "user-requested erasure").
    #[serde(default)]
    pub reason: String,
}

/// How a column's values should be transformed before being returned.
/// `Mask` is the most common — keeps a few visible chars, replaces the
/// rest with `*`. `Hash` returns SHA-256 of the value for join keys you
/// want to preserve referential integrity on without exposing the value.
/// `Null` returns NULL unconditionally.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum Redaction {
    Null,
    Hash,
    Mask {
        /// Number of leading characters left visible.
        keep_prefix: usize,
        /// Number of trailing characters left visible.
        keep_suffix: usize,
    },
}
