use crate::error::DalError;
use crate::models::strategy::StrategyRow;
use sqlx::PgPool;
use uuid::Uuid;

const COLS: &str = "id, hash, config, hash_input, created_at, name, version, description";

/// Upsert a strategy by hash (anonymous / API-driven path).
///
/// If a strategy with this hash already exists, returns the existing row
/// (preserving the original id and created_at). Name and description are
/// not overwritten by this path — use `upsert_named` for seeded strategies.
pub async fn upsert(
    pool: &PgPool,
    hash: &str,
    config: &serde_json::Value,
    hash_input: &serde_json::Value,
) -> Result<StrategyRow, DalError> {
    let sql = format!(
        "INSERT INTO strategies (hash, config, hash_input)
         VALUES ($1, $2, $3)
         ON CONFLICT (hash) DO UPDATE SET config = EXCLUDED.config
         RETURNING {COLS}"
    );
    let row = sqlx::query_as::<_, StrategyRow>(&sql)
        .bind(hash)
        .bind(config)
        .bind(hash_input)
        .fetch_one(pool)
        .await?;

    Ok(row)
}

/// Upsert a named strategy by (name, version) — used by the seed runner.
///
/// If a strategy with this name+version already exists, updates the description,
/// config, and hash (allowing seed re-runs after config changes). The original
/// id and created_at are preserved.
///
/// If a row with the same hash already exists (e.g. an anonymous API-submitted
/// strategy), that row is adopted: its name/version/description are updated in
/// place so the seed "claims" the existing content-dedup row rather than
/// conflicting with the hash UNIQUE constraint.
pub async fn upsert_named(
    pool: &PgPool,
    name: &str,
    version: i32,
    description: &str,
    hash: &str,
    config: &serde_json::Value,
    hash_input: &serde_json::Value,
) -> Result<StrategyRow, DalError> {
    // Two-phase CTE:
    //   1. UPDATE any row whose hash already matches — adopts anonymous or
    //      differently-named rows so we never hit the hash UNIQUE constraint.
    //   2. INSERT (with name+version conflict handling) only when no such row
    //      existed.
    let sql = format!(
        "WITH hash_match AS (
             UPDATE strategies
             SET name        = $1,
                 version     = $2,
                 description = $3,
                 config      = $5,
                 hash_input  = $6
             WHERE hash = $4
             RETURNING {COLS}
         ),
         name_version_upsert AS (
             INSERT INTO strategies (name, version, description, hash, config, hash_input)
             SELECT $1, $2, $3, $4, $5, $6
             WHERE NOT EXISTS (SELECT 1 FROM hash_match)
             ON CONFLICT (name, version) WHERE name IS NOT NULL
             DO UPDATE SET
                 description = EXCLUDED.description,
                 hash        = EXCLUDED.hash,
                 config      = EXCLUDED.config,
                 hash_input  = EXCLUDED.hash_input
             RETURNING {COLS}
         )
         SELECT {COLS} FROM hash_match
         UNION ALL
         SELECT {COLS} FROM name_version_upsert
         LIMIT 1"
    );
    let row = sqlx::query_as::<_, StrategyRow>(&sql)
        .bind(name)
        .bind(version)
        .bind(description)
        .bind(hash)
        .bind(config)
        .bind(hash_input)
        .fetch_one(pool)
        .await?;

    Ok(row)
}

/// Get a strategy by id.
pub async fn get_by_id(pool: &PgPool, id: Uuid) -> Result<Option<StrategyRow>, DalError> {
    let sql = format!("SELECT {COLS} FROM strategies WHERE id = $1");
    let row = sqlx::query_as::<_, StrategyRow>(&sql)
        .bind(id)
        .fetch_optional(pool)
        .await?;

    Ok(row)
}

/// Get a strategy by its content hash.
pub async fn get_by_hash(pool: &PgPool, hash: &str) -> Result<Option<StrategyRow>, DalError> {
    let sql = format!("SELECT {COLS} FROM strategies WHERE hash = $1");
    let row = sqlx::query_as::<_, StrategyRow>(&sql)
        .bind(hash)
        .fetch_optional(pool)
        .await?;

    Ok(row)
}

/// List strategies with pagination, most recently created first.
pub async fn list(
    pool: &PgPool,
    limit: i64,
    offset: i64,
) -> Result<Vec<StrategyRow>, DalError> {
    let sql = format!(
        "SELECT {COLS} FROM strategies ORDER BY created_at DESC LIMIT $1 OFFSET $2"
    );
    let rows = sqlx::query_as::<_, StrategyRow>(&sql)
        .bind(limit)
        .bind(offset)
        .fetch_all(pool)
        .await?;

    Ok(rows)
}
