use crate::config::constants::{DEV_FEE_AUDIT_EVENT_KIND, DEV_FEE_LIGHTNING_ADDRESS};
use crate::config::settings::{get_db_pool, Settings};
use crate::config::*;
use crate::db;
use crate::db::is_user_present;
use crate::flow;
use crate::lightning;
use crate::lightning::invoice::is_valid_invoice;
use crate::lightning::LndConnector;
use crate::lnurl::HTTP_CLIENT;
use crate::messages;
use crate::models::Yadio;
use crate::nip33::{create_platform_tag_values, new_order_event, new_rating_event, order_to_tags};
use crate::NOSTR_CLIENT;

use chrono::Duration;
use fedimint_tonic_lnd::lnrpc::invoice::InvoiceState;
use mostro_core::prelude::*;
use nostr_sdk::prelude::*;
use sqlx::Pool;
use sqlx::QueryBuilder;
use sqlx::Sqlite;
use sqlx::SqlitePool;
use sqlx_crud::Crud;
use std::collections::HashMap;
use std::fmt::Write;
use std::str::FromStr;
use std::thread;
use tokio::sync::mpsc::channel;
use tracing::info;
use uuid::Uuid;

pub type FiatNames = std::collections::HashMap<String, String>;
const MAX_RETRY: u16 = 4;

// Redefined for convenience
type OrderKind = mostro_core::order::Kind;

/// Resolve the Yadio base URL for the live market-quote path (`/convert`,
/// `/currencies`).
///
/// Phase 1 transition (spec §10.1): the cached aggregate path reads
/// `[price.providers.yadio].url`, but this live path historically read the
/// legacy `[mostro].bitcoin_price_api_url`. If an operator customises only
/// the new key, the two paths would silently hit different Yadio bases.
/// Prefer the configured Yadio provider URL when it is *usable* — the provider
/// is enabled and has a non-empty URL — otherwise fall back to the legacy key.
/// A disabled or blank provider entry must not suppress that fallback. The
/// chosen URL is normalized exactly as [`YadioProvider::new`] does
/// (`trim_end_matches('/')`) so appending `/convert` / `/currencies` can never
/// produce a `//`, keeping the live and aggregate paths on an identical base.
/// Phase 4 removes this live HTTP path entirely, at which point the legacy key
/// only feeds legacy synthesis.
fn yadio_base_url() -> String {
    let provider = Settings::get_price().and_then(|price| {
        price
            .providers
            .get(&crate::price::ProviderId::Yadio.to_string())
            .map(|yadio| (yadio.url.as_str(), yadio.enabled))
    });
    let legacy = Settings::get_mostro().bitcoin_price_api_url.clone();
    select_yadio_base_url(provider, &legacy)
}

/// Drop surrounding whitespace and any trailing slash, matching
/// [`crate::price::providers::yadio::YadioProvider::new`]. A URL that is only
/// slashes/whitespace normalizes to empty and is treated as "not configured".
fn normalize_base_url(raw: &str) -> String {
    raw.trim().trim_end_matches('/').to_string()
}

/// Pure selection logic behind [`yadio_base_url`], split out so it is unit
/// testable without the write-once global `Settings`. `provider` is
/// `(url, enabled)` for the `[price.providers.yadio]` entry when present.
/// Prefer that URL only when it is *usable* — the provider is enabled and the
/// URL is non-empty after normalization — otherwise fall back to the
/// (normalized) legacy `bitcoin_price_api_url`.
fn select_yadio_base_url(provider: Option<(&str, bool)>, legacy: &str) -> String {
    if let Some((url, enabled)) = provider {
        if enabled {
            let url = normalize_base_url(url);
            if !url.is_empty() {
                return url;
            }
        }
    }
    normalize_base_url(legacy)
}

pub async fn retries_yadio_request(
    req_string: &str,
    fiat_code: &str,
) -> Result<(Option<reqwest::Response>, bool), MostroError> {
    // Get Fiat list and check if currency exchange is available
    let api_req_string = format!("{}/currencies", yadio_base_url());
    let fiat_list_check = HTTP_CLIENT
        .get(api_req_string)
        .send()
        .await
        .map_err(|_| MostroInternalErr(ServiceError::NoAPIResponse))?
        .json::<FiatNames>()
        .await
        .map_err(|_| MostroInternalErr(ServiceError::MalformedAPIRes))?
        .contains_key(fiat_code);

    // Exit with error - no currency
    if !fiat_list_check {
        return Ok((None, fiat_list_check));
    }

    let res = HTTP_CLIENT
        .get(req_string)
        .send()
        .await
        .map_err(|_| MostroInternalErr(ServiceError::NoAPIResponse))?;

    Ok((Some(res), fiat_list_check))
}

pub fn get_bitcoin_price(fiat_code: &str) -> Result<f64, MostroError> {
    crate::price::get_bitcoin_price(fiat_code)
}

/// Request market quote from Yadio to have sats amount at actual market price
pub async fn get_market_quote(
    fiat_amount: &i64,
    fiat_code: &str,
    premium: i64,
) -> Result<i64, MostroError> {
    // Add here check for market price
    let req_string = format!(
        "{}/convert/{}/{}/BTC",
        yadio_base_url(),
        fiat_amount,
        fiat_code
    );
    info!("Requesting API price: {}", req_string);

    let mut req = (None, false);
    let mut no_answer_api = false;

    // Retry for 4 times
    for retries_num in 1..=MAX_RETRY {
        match retries_yadio_request(&req_string, fiat_code).await {
            Ok(response) => {
                req = response;
                break;
            }
            Err(_e) => {
                if retries_num == MAX_RETRY {
                    no_answer_api = true;
                }
                println!(
                    "API price request failed retrying - {} tentatives left.",
                    (MAX_RETRY - retries_num)
                );
                thread::sleep(std::time::Duration::from_secs(2));
            }
        };
    }

    // Case no answers from Yadio
    if no_answer_api {
        return Err(MostroError::MostroInternalErr(ServiceError::NoAPIResponse));
    }

    // No currency present
    if !req.1 {
        return Err(MostroError::MostroInternalErr(ServiceError::NoCurrency));
    }

    if req.0.is_none() {
        return Err(MostroError::MostroInternalErr(
            ServiceError::MalformedAPIRes,
        ));
    }

    let quote = if let Some(q) = req.0 {
        q.json::<Yadio>()
            .await
            .map_err(|_| MostroError::MostroInternalErr(ServiceError::MessageSerializationError))?
    } else {
        return Err(MostroError::MostroInternalErr(
            ServiceError::MalformedAPIRes,
        ));
    };

    let mut sats = quote.result * 100_000_000_f64;

    // Added premium value to have correct sats value
    if premium != 0 {
        sats = sats - (premium as f64) / 100_f64 * sats;
    }

    Ok(sats as i64)
}

pub fn get_fee(amount: i64) -> i64 {
    let mostro_settings = Settings::get_mostro();
    // We calculate the bot fee
    let split_fee = (mostro_settings.fee * amount as f64) / 2.0;
    split_fee.round() as i64
}

/// Calculates the development fee as a percentage of the total Mostro fee.
///
/// This is a pure function that performs the fee calculation without accessing global state.
/// Useful for testing with different percentage values.
///
/// # Arguments
/// * `total_mostro_fee` - The total Mostro fee amount in satoshis
/// * `percentage` - The percentage to apply (e.g., 0.30 for 30%)
///
/// # Returns
/// The calculated development fee, rounded to nearest satoshi
pub fn calculate_dev_fee(total_mostro_fee: i64, percentage: f64) -> i64 {
    let dev_fee = (total_mostro_fee as f64) * percentage;
    dev_fee.round() as i64
}

/// Calculate total development fee from the total Mostro fee
/// Takes the TOTAL Mostro fee (both parties combined) and returns the TOTAL dev fee
/// The returned value should be split 50/50 between buyer and seller
/// Returns the total amount in satoshis for the dev fund
pub fn get_dev_fee(total_mostro_fee: i64) -> i64 {
    let mostro_settings = Settings::get_mostro();
    calculate_dev_fee(total_mostro_fee, mostro_settings.dev_fee_percentage)
}

/// Calculates the expiration timestamp for an order.
///
/// This function computes the expiration time based on the current time and application settings.
/// If an expiration timestamp is provided, it is clamped to a maximum allowed value (the current time plus
/// a configured maximum number of days). If no timestamp is given, a default expiration is calculated as the
/// current time plus a configured number of hours.
///
/// # Returns
///
/// The computed expiration timestamp as a Unix epoch in seconds.
///
/// # Examples
///
/// ```
/// // Calculate a default expiration timestamp.
/// let exp_default = get_expiration_date(None);
/// println!("Default expiration: {}", exp_default);
///
/// // Provide a custom expiration timestamp. The returned value will be clamped
/// // if it exceeds the maximum allowed expiration.
/// let exp_custom = get_expiration_date(Some(exp_default + 10_000));
/// println!("Custom expiration (clamped if necessary): {}", exp_custom);
/// ```
pub fn get_expiration_date(expire: Option<i64>) -> i64 {
    let mostro_settings = Settings::get_mostro();
    // We calculate order expiration
    let expire_date: i64;
    let expires_at_max: i64 = Timestamp::now().as_secs() as i64
        + Duration::days(mostro_settings.max_expiration_days.into()).num_seconds();
    if let Some(mut exp) = expire {
        if exp > expires_at_max {
            exp = expires_at_max;
        };
        expire_date = exp;
    } else {
        expire_date = Timestamp::now().as_secs() as i64
            + Duration::hours(mostro_settings.expiration_hours as i64).num_seconds();
    }
    expire_date
}

/// Get expiration timestamp for an event kind based on expiration configuration
///
/// This function calculates the expiration timestamp for different event kinds
/// using the configured expiration days per kind. Falls back to max_expiration_days
/// if no expiration configuration is available (backward compatibility).
///
/// # Arguments
///
/// * `kind` - The event kind (38383 for orders, 38384 for ratings, 38386 for disputes, 8383 for fee audits)
///
/// # Returns
///
/// * `Some(i64)` - Unix timestamp when the event should expire
/// * `None` - If the event kind should not have expiration
///
/// # Examples
///
/// ```
/// // Get expiration for a dispute event (kind 38386)  
/// let dispute_expiration = get_expiration_timestamp_for_kind(38386);
/// ```
pub fn get_expiration_timestamp_for_kind(kind: u16) -> Option<i64> {
    let now = Timestamp::now().as_secs() as i64;

    // Try to get expiration from new configuration first
    if let Some(exp_config) = Settings::get_expiration() {
        if let Some(days) = exp_config.get_expiration_for_kind(kind) {
            return Some(now + Duration::days(days as i64).num_seconds());
        }
    }

    // Backward-compat fallback for known kinds only.
    // Keep this list in sync with `ExpirationSettings::get_expiration_for_kind` in `src/config/types.rs`
    // when adding/removing event kinds.
    match kind {
        NOSTR_ORDER_EVENT_KIND
        | NOSTR_RATING_EVENT_KIND
        | NOSTR_DISPUTE_EVENT_KIND
        | DEV_FEE_AUDIT_EVENT_KIND => {
            let mostro_settings = Settings::get_mostro();
            Some(now + Duration::days(mostro_settings.max_expiration_days.into()).num_seconds())
        }
        _ => None,
    }
}

/// Checks whether an order qualifies as a full privacy order and returns corresponding event tags.
///
/// This asynchronous function verifies whether the user associated with the order exists in the database.
/// If the user is found, the order is converted to tags including user metadata (total rating, total reviews, and creation date).
/// If not, the function checks that the identity and trade public keys match, and if so, converts the order without user data;
/// otherwise, it returns an error indicating an invalid public key.
///
/// # Errors
///
/// Returns a `MostroInternalErr(ServiceError::InvalidPubkey)` if no user data is found and the identity public key does not match
/// the trade public key.
///
/// # Examples
///
/// ```rust
/// # async fn example() -> Result<(), MostroError> {
/// // Assume proper initialization of the order, pool, and public keys.
/// let order = Order { /* initialize order fields */ };
/// let pool = SqlitePool::connect("sqlite://:memory:").await.unwrap();
/// let identity_pubkey = PublicKey::from_str("02abcdef...").unwrap();
/// let trade_pubkey = identity_pubkey.clone();
///
/// let tags = get_tags_for_new_order(&order, &pool, &identity_pubkey, &trade_pubkey, &keys).await?;
/// // Use `tags` for further event processing.
/// # Ok(())
/// # }
pub async fn get_tags_for_new_order(
    new_order_db: &Order,
    pool: &SqlitePool,
    identity_pubkey: &PublicKey,
    trade_pubkey: &PublicKey,
    mostro_keys: &Keys,
) -> Result<Option<Tags>, MostroError> {
    let mostro_pubkey = mostro_keys.public_key().to_hex();
    match is_user_present(pool, identity_pubkey.to_string()).await {
        Ok(user) => {
            // We transform the order fields to tags to use in the event
            order_to_tags(
                new_order_db,
                Some((user.total_rating, user.total_reviews, user.created_at)),
                Some(&mostro_pubkey),
            )
        }
        Err(_) => {
            // We transform the order fields to tags to use in the event
            if identity_pubkey == trade_pubkey {
                order_to_tags(new_order_db, Some((0.0, 0, 0)), Some(&mostro_pubkey))
            } else {
                Err(MostroInternalErr(ServiceError::InvalidPubkey))
            }
        }
    }
}

#[allow(clippy::too_many_arguments)]
/// Publishes a new order by preparing its details, saving it to the database, creating a corresponding Nostr event, and sending a confirmation message.
///
/// This asynchronous function performs the following steps:
/// - Prepares a new order record from the provided order data and public keys.
/// - Inserts the new order into the database.
/// - Determines order tags based on privacy settings using `check_full_privacy_order`.
/// - Constructs and publishes a Nostr event representing the order.
/// - Updates the order record with the generated event ID.
/// - Enqueues an acknowledgement message for the order.
///
/// # Examples
///
/// ```rust
/// # async fn example() -> Result<(), MostroError> {
/// # use sqlx::sqlite::SqlitePool;
/// # use nostr::Keys;
/// # use my_crate::{SmallOrder, publish_order};
/// // Initialize the database pool and keys.
/// let pool = SqlitePool::connect("sqlite::memory:").await.unwrap();
/// let keys = Keys::generate();
///
/// // Prepare a new order along with associated public keys.
/// let new_order = SmallOrder::default();
/// let initiator_pubkey = /* initiator public key */;
/// let identity_pubkey = /* identity public key */;
/// let trade_pubkey = /* trade public key */;
/// let request_id = Some(100);
/// let trade_index = Some(1);
///
/// publish_order(&pool, &keys, &new_order, initiator_pubkey, identity_pubkey, trade_pubkey, request_id, trade_index).await?;
/// # Ok(())
/// # }
/// ```
pub async fn publish_order(
    pool: &SqlitePool,
    keys: &Keys,
    new_order: &SmallOrder,
    initiator_pubkey: PublicKey,
    identity_pubkey: PublicKey,
    trade_pubkey: PublicKey,
    request_id: Option<u64>,
    trade_index: Option<i64>,
) -> Result<(), MostroError> {
    // Prepare a new default order
    let mut new_order_db = match prepare_new_order(
        new_order,
        initiator_pubkey,
        trade_index,
        identity_pubkey,
        trade_pubkey,
    )
    .await
    {
        Ok(order) => order,
        Err(e) => {
            return Err(e);
        }
    };

    // Phase 5/6: when the maker side is bonded, the order must NOT hit the
    // order book until the maker locks an anti-abuse bond. Park it at
    // `WaitingMakerBond` (no NIP-33 event emitted), request the bond, and
    // defer the publication to `resume_publish_after_maker_bond`, which
    // the bond subscriber calls on `Accepted`. Both fixed-amount (Phase 5)
    // and range (Phase 6) orders take this path; range orders size the
    // bond against `max_amount` (worst-case exposure) and resolve slashes
    // proportionally per taken slice — see `maker_bond_notional_sats`.
    let maker_bond_required = crate::app::bond::maker_bond_required();
    if maker_bond_required {
        let notional = maker_bond_notional_sats(&new_order_db)?;
        new_order_db.status = Status::WaitingMakerBond.to_string();
        let order = new_order_db
            .create(pool)
            .await
            .map_err(|e| MostroInternalErr(ServiceError::DbAccessError(e.to_string())))?;
        info!("New order saved (awaiting maker bond) Id: {}", order.id);
        if let Err(e) = crate::app::bond::request_maker_bond(
            pool,
            &order,
            trade_pubkey,
            notional,
            request_id,
            trade_index,
        )
        .await
        {
            // The order was parked at `WaitingMakerBond` but never emitted
            // a NIP-33 event, and `request_maker_bond` already released any
            // bond row it managed to create. Without cleanup the row would
            // sit hidden in `WaitingMakerBond` until the order-expiry job
            // reaps it hours later. Delete the stranded row now (scoped to
            // the parked status so we never touch one that has since
            // advanced) and surface the error to the maker.
            tracing::warn!(
                order_id = %order.id,
                "publish_order: request_maker_bond failed ({}); deleting stranded WaitingMakerBond order",
                e
            );
            if let Err(del) = sqlx::query("DELETE FROM orders WHERE id = ? AND status = ?")
                .bind(order.id)
                .bind(Status::WaitingMakerBond.to_string())
                .execute(pool)
                .await
            {
                tracing::warn!(
                    order_id = %order.id,
                    "publish_order: failed to delete stranded order: {}", del
                );
            }
            return Err(e);
        }
        return Ok(());
    }

    // CRUD order creation
    let order = new_order_db
        .create(pool)
        .await
        .map_err(|e| MostroInternalErr(ServiceError::DbAccessError(e.to_string())))?;
    info!("New order saved Id: {}", order.id);

    finalize_order_publication(
        pool,
        keys,
        order,
        identity_pubkey,
        trade_pubkey,
        request_id,
        trade_index,
    )
    .await
}

/// Sats notional a maker bond is sized against.
///
/// - **Range orders (Phase 6).** Sized against `max_amount` — the
///   worst-case fiat exposure the maker is advertising — converted at the
///   current cached price. Each taken slice later slashes a proportional
///   share of the resulting bond (`slice.fiat_amount / max_amount`), so
///   the notional must be the range ceiling, not any single slice.
/// - **Fixed-price orders.** Carry their sats `amount` directly.
/// - **Market-priced single orders.** `amount == 0` at creation, so we
///   convert the fiat amount at the current cached price — the same quote
///   `calculate_and_check_quote` validates against at order time.
///
/// The bond is a one-time snapshot and is not repriced if the market moves
/// before the order is taken (spec §10.3).
fn maker_bond_notional_sats(order: &Order) -> Result<i64, MostroError> {
    // Range orders: size against the fiat ceiling (`max_amount`).
    if order.is_range_order() {
        // `is_range_order()` only checks that `min`/`max` are `Some`, not
        // that they are positive, so guard against a zero/negative ceiling
        // here — a non-positive `max_amount` would otherwise size the bond
        // at the floor and later divide-by-zero in the proportional slash
        // (`record_maker_slice_slash`, which carries the matching guard).
        let max_fiat = order.max_amount.filter(|m| *m > 0).ok_or_else(|| {
            MostroInternalErr(ServiceError::UnexpectedError(
                "range order missing positive max_amount".to_string(),
            ))
        })?;
        let price = get_bitcoin_price(&order.fiat_code)?;
        if price <= 0.0 {
            return Err(MostroInternalErr(ServiceError::NoAPIResponse));
        }
        let sats = (max_fiat as f64 / price) * 1E8;
        return Ok(sats as i64);
    }
    if order.amount > 0 {
        return Ok(order.amount);
    }
    let price = get_bitcoin_price(&order.fiat_code)?;
    if price <= 0.0 {
        return Err(MostroInternalErr(ServiceError::NoAPIResponse));
    }
    let sats = (order.fiat_amount as f64 / price) * 1E8;
    Ok(sats as i64)
}

/// Publish the NIP-33 event for a freshly-persisted order, persist its
/// `event_id`, ack the maker with [`Action::NewOrder`], and broadcast.
///
/// Shared by the inline `publish_order` path (no maker bond) and the
/// deferred [`resume_publish_after_maker_bond`] path (maker bond locked).
/// The order row must already exist in the DB; on success it is in
/// `Status::Pending` with its `event_id` set.
async fn finalize_order_publication(
    pool: &SqlitePool,
    keys: &Keys,
    mut order: Order,
    identity_pubkey: PublicKey,
    trade_pubkey: PublicKey,
    request_id: Option<u64>,
    trade_index: Option<i64>,
) -> Result<(), MostroError> {
    let order_id = order.id;
    // The maker-bond path parked the order at `WaitingMakerBond`; the
    // no-bond path created it at `Pending`. Either way it goes live now.
    order.status = Status::Pending.to_string();

    // Get tags for new order in case of full privacy or normal order
    // nip33 kind with order fields as tags and order id as identifier (kind 38383 for orders)
    let event = if let Some(tags) =
        get_tags_for_new_order(&order, pool, &identity_pubkey, &trade_pubkey, keys).await?
    {
        new_order_event(keys, "", order_id.to_string(), tags)
            .map_err(|e| MostroInternalErr(ServiceError::NostrError(e.to_string())))?
    } else {
        return Err(MostroInternalErr(ServiceError::InvalidPubkey));
    };

    info!("Order event to be published: {event:#?}");
    let event_id = event.id.to_string();
    info!("Publishing Event Id: {event_id} for Order Id: {order_id}");
    // We update the order with the new event_id (and Pending status)
    order.event_id = event_id;
    // Build the ack payload before `update` consumes the order row.
    let mut small = order.as_new_order();
    small.id = Some(order_id);
    order
        .update(pool)
        .await
        .map_err(|e| MostroInternalErr(ServiceError::DbAccessError(e.to_string())))?;

    // Send message as ack with small order
    enqueue_order_msg(
        request_id,
        Some(order_id),
        Action::NewOrder,
        Some(Payload::Order(small)),
        trade_pubkey,
        trade_index,
    )
    .await;

    NOSTR_CLIENT
        .get()
        .unwrap()
        .send_event(&event)
        .await
        .map(|_s| ())
        .map_err(|err| MostroInternalErr(ServiceError::NostrError(err.to_string())))
}

/// Finish publishing an order whose maker bond has just locked.
///
/// Called from the bond subscriber (`bond::flow::on_maker_bond_accepted`).
/// Derives the maker's identity and trade pubkeys from the order row —
/// the maker is the seller on a sell order, the buyer on a buy order
/// (§3.1) — and hands off to [`finalize_order_publication`]. Idempotency
/// across redeliveries is enforced by the caller, which only invokes this
/// while the order is still in `WaitingMakerBond`.
pub async fn resume_publish_after_maker_bond(
    pool: &SqlitePool,
    keys: &Keys,
    order: Order,
    request_id: Option<u64>,
) -> Result<(), MostroError> {
    // Atomically claim the deferred `WaitingMakerBond → Pending`
    // transition. The bond subscriber already re-read the row and saw
    // `WaitingMakerBond`, but that check is not atomic with the publish
    // below: the order-expiry job (`job_expire_pending_older_orders`) can
    // flip the row `WaitingMakerBond → Expired` (and cancel the just-locked
    // bond) in between. Without a CAS the full-row write inside
    // `finalize_order_publication` would blindly resurrect the dead order
    // back to `Pending` and emit a NIP-33 event for it. If the CAS affects
    // 0 rows another path already owns the status, so we skip cleanly.
    let cas = sqlx::query("UPDATE orders SET status = ? WHERE id = ? AND status = ?")
        .bind(Status::Pending.to_string())
        .bind(order.id)
        .bind(Status::WaitingMakerBond.to_string())
        .execute(pool)
        .await
        .map_err(|e| MostroInternalErr(ServiceError::DbAccessError(e.to_string())))?;
    if cas.rows_affected() != 1 {
        info!(
            "resume_publish_after_maker_bond: order {} no longer WaitingMakerBond — skipping deferred publish",
            order.id
        );
        return Ok(());
    }
    let kind = order.get_order_kind().map_err(MostroInternalErr)?;
    let (trade_pubkey, identity_pubkey, trade_index) = match kind {
        OrderKind::Sell => (
            order.get_seller_pubkey().map_err(MostroInternalErr)?,
            order
                .get_master_seller_pubkey()
                .map_err(MostroInternalErr)?,
            order.trade_index_seller,
        ),
        OrderKind::Buy => (
            order.get_buyer_pubkey().map_err(MostroInternalErr)?,
            order.get_master_buyer_pubkey().map_err(MostroInternalErr)?,
            order.trade_index_buyer,
        ),
    };
    finalize_order_publication(
        pool,
        keys,
        order,
        identity_pubkey,
        trade_pubkey,
        request_id,
        trade_index,
    )
    .await
}

async fn prepare_new_order(
    new_order: &SmallOrder,
    initiator_pubkey: PublicKey,
    trade_index: Option<i64>,
    identity_pubkey: PublicKey,
    trade_pubkey: PublicKey,
) -> Result<Order, MostroError> {
    let mut fee = 0;
    // dev_fee is always calculated when the order is taken, not at creation time
    // This unifies the behavior for both fixed price and market price orders
    let dev_fee = 0;
    if new_order.amount > 0 {
        fee = get_fee(new_order.amount); // Get split fee (each party's share)
                                         // dev_fee will be calculated in take_buy_action() or take_sell_action()
    }

    // Get expiration time of the order
    let expiry_date = get_expiration_date(new_order.expires_at);

    // Prepare a new default order
    let mut new_order_db = Order {
        id: Uuid::new_v4(),
        kind: OrderKind::Sell.to_string(),
        status: Status::Pending.to_string(),
        creator_pubkey: initiator_pubkey.to_string(),
        payment_method: new_order.payment_method.clone(),
        amount: new_order.amount,
        fee,
        dev_fee,
        dev_fee_paid: false,
        dev_fee_payment_hash: None,
        fiat_code: new_order.fiat_code.clone(),
        min_amount: new_order.min_amount,
        max_amount: new_order.max_amount,
        fiat_amount: new_order.fiat_amount,
        premium: new_order.premium,
        buyer_invoice: new_order.buyer_invoice.clone(),
        created_at: Timestamp::now().as_secs() as i64,
        expires_at: expiry_date,
        ..Default::default()
    };

    match new_order.kind {
        Some(OrderKind::Buy) => {
            new_order_db.kind = OrderKind::Buy.to_string();
            new_order_db.buyer_pubkey = Some(trade_pubkey.to_string());
            new_order_db.master_buyer_pubkey = Some(identity_pubkey.to_string());
            new_order_db.trade_index_buyer = trade_index;
        }
        Some(OrderKind::Sell) => {
            new_order_db.kind = OrderKind::Sell.to_string();
            new_order_db.seller_pubkey = Some(trade_pubkey.to_string());
            new_order_db.master_seller_pubkey = Some(identity_pubkey.to_string());
            new_order_db.trade_index_seller = trade_index;
        }
        None => {
            return Err(MostroCantDo(CantDoReason::InvalidOrderKind));
        }
    }

    // Request price from API in case amount is 0
    new_order_db.price_from_api = new_order.amount == 0;
    Ok(new_order_db)
}

pub async fn send_dm(
    receiver_pubkey: PublicKey,
    sender_keys: &Keys,
    payload: &str,
    expiration: Option<Timestamp>,
) -> Result<(), MostroError> {
    info!(
        "sender key {} - receiver key {}",
        sender_keys.public_key().to_hex(),
        receiver_pubkey.to_hex()
    );
    let message = Message::from_json(payload)
        .map_err(|_| MostroInternalErr(ServiceError::MessageSerializationError))?;

    // Mostro node holds a single keypair: it doubles as identity and trade key.
    // Server-originated messages are unsigned because clients don't track a
    // trade_index for the node.
    let event = wrap_message(
        &message,
        sender_keys,
        sender_keys,
        receiver_pubkey,
        WrapOptions {
            signed: false,
            expiration,
            ..WrapOptions::default()
        },
    )
    .await?;

    info!(
        "Sending message, Event ID: {} to {} with payload: {:#?}",
        event.id,
        receiver_pubkey.to_hex(),
        payload
    );

    if let Ok(client) = get_nostr_client() {
        client
            .send_event(&event)
            .await
            .map_err(|e| MostroInternalErr(ServiceError::NostrError(e.to_string())))?;
    }

    Ok(())
}

/// Publishes a dev fee payment audit event to Nostr relays
///
/// This function creates and publishes a Nostr event (kind 8383) containing
/// audit information about a successful dev fee payment. The event includes
/// payment details for transparency and third-party verification.
///
/// # Arguments
/// * `order` - The order for which dev fee was paid
/// * `payment_hash` - The Lightning Network payment hash
///
/// # Returns
/// * `Ok(())` if event was published successfully
/// * `Err(MostroError)` if publishing failed
///
/// # Privacy
/// This function does NOT include buyer or seller pubkeys to maintain user privacy.
/// Only aggregate payment data and order metadata are published.
pub async fn publish_dev_fee_audit_event(
    order: &Order,
    payment_hash: &str,
) -> Result<(), MostroError> {
    use std::borrow::Cow;
    let ln_network = match LN_STATUS.get() {
        Some(status) => status.networks.join(","),
        None => "unknown".to_string(),
    };
    // Get Mostro keys for signing
    let keys = get_keys()?;

    // Get Nostr client
    let client = get_nostr_client()?;

    // Create tags for queryability
    let mut tag_list = vec![
        Tag::custom(
            TagKind::Custom(Cow::Borrowed("order-id")),
            vec![order.id.to_string()],
        ),
        Tag::custom(
            TagKind::Custom(Cow::Borrowed("amount")),
            vec![order.dev_fee.to_string()],
        ),
        Tag::custom(
            TagKind::Custom(Cow::Borrowed("hash")),
            vec![payment_hash.to_string()],
        ),
        Tag::custom(
            TagKind::Custom(Cow::Borrowed("destination")),
            vec![DEV_FEE_LIGHTNING_ADDRESS.to_string()],
        ),
        Tag::custom(TagKind::Custom(Cow::Borrowed("network")), vec![ln_network]),
        Tag::custom(
            TagKind::Custom(Cow::Borrowed("y")),
            create_platform_tag_values(Settings::get_mostro().name.as_deref()),
        ),
        Tag::custom(
            TagKind::Custom(Cow::Borrowed("z")),
            vec!["dev-fee-payment".to_string()],
        ),
    ];

    // Add expiration tag if configured
    if let Some(expiration_timestamp) = get_expiration_timestamp_for_kind(DEV_FEE_AUDIT_EVENT_KIND)
    {
        tag_list.push(Tag::expiration(Timestamp::from(
            expiration_timestamp as u64,
        )));
    }

    let tags = Tags::from_list(tag_list);

    // Create and sign event
    let event = EventBuilder::new(nostr_sdk::Kind::Custom(DEV_FEE_AUDIT_EVENT_KIND), "")
        .tags(tags)
        .sign_with_keys(&keys)
        .map_err(|e| MostroInternalErr(ServiceError::NostrError(e.to_string())))?;

    // Publish event to relays
    client
        .send_event(&event)
        .await
        .map_err(|e| MostroInternalErr(ServiceError::NostrError(e.to_string())))?;

    info!(
        "📡 Published dev fee audit event for order {} - {} sats to relays",
        order.id, order.dev_fee
    );

    Ok(())
}

pub fn get_keys() -> Result<Keys, MostroError> {
    let nostr_settings = Settings::get_nostr();
    // nostr private key
    match Keys::parse(&nostr_settings.nsec_privkey) {
        Ok(my_keys) => Ok(my_keys),
        Err(e) => {
            tracing::error!("Failed to parse nostr private key: {}", e);
            Err(MostroInternalErr(ServiceError::NostrError(e.to_string())))
        }
    }
}

#[allow(clippy::too_many_arguments)]
pub async fn update_user_rating_event(
    user: &str,
    buyer_sent_rate: bool,
    seller_sent_rate: bool,
    tags: Tags,
    msg: &Message,
    keys: &Keys,
    pool: &SqlitePool,
) -> Result<()> {
    // Get order from msg
    let mut order = get_order(msg, pool).await?;

    // nip33 kind with user as identifier (kind 38384 for ratings)
    let event = new_rating_event(keys, "", user.to_string(), tags)?;
    info!("Sending replaceable event: {event:#?}");
    // We update the order vote status
    if buyer_sent_rate {
        order.buyer_sent_rate = buyer_sent_rate;
    }
    if seller_sent_rate {
        order.seller_sent_rate = seller_sent_rate;
    }
    order.update(pool).await?;

    // Add event message to global list
    MESSAGE_QUEUES.queue_order_rate.write().await.push(event);
    Ok(())
}

async fn get_ratings_for_pending_order(
    order_updated: &Order,
    status: Status,
) -> Result<Option<(f64, i64, i64)>, MostroError> {
    // Phase 1.5: `WaitingTakerBond` publishes on the wire as `pending`
    // (see `nip33::create_status_tags`), so the maker rating must travel
    // with both buckets — otherwise clients browsing the orderbook would
    // see the order without ratings during the bond window.
    if status == Status::Pending || status == Status::WaitingTakerBond {
        let identity_pubkey = match order_updated.is_sell_order() {
            Ok(_) => order_updated
                .get_master_seller_pubkey()
                .map_err(MostroInternalErr)?,
            Err(_) => order_updated
                .get_master_buyer_pubkey()
                .map_err(MostroInternalErr)?,
        };

        let trade_pubkey = match order_updated.is_sell_order() {
            Ok(_) => order_updated
                .get_seller_pubkey()
                .map_err(MostroInternalErr)?,
            Err(_) => order_updated
                .get_buyer_pubkey()
                .map_err(MostroInternalErr)?,
        };

        match is_user_present(&get_db_pool(), identity_pubkey.to_string()).await {
            Ok(user) => Ok(Some((
                user.total_rating,
                user.total_reviews,
                user.created_at,
            ))),
            Err(_) => {
                if identity_pubkey == trade_pubkey {
                    Ok(Some((0.0, 0, 0)))
                } else {
                    Err(MostroInternalErr(ServiceError::InvalidPubkey))
                }
            }
        }
    } else {
        Ok(None)
    }
}

pub async fn update_order_event(
    keys: &Keys,
    status: Status,
    order: &Order,
) -> Result<Order, MostroError> {
    let mut order_updated = order.clone();
    // update order.status with new status
    order_updated.status = status.to_string();

    // Include rating tag for pending orders
    let reputation_data = get_ratings_for_pending_order(&order_updated, status).await?;

    // We transform the order fields to tags to use in the event
    let mostro_pubkey = keys.public_key().to_hex();
    if let Some(tags) = order_to_tags(&order_updated, reputation_data, Some(&mostro_pubkey))? {
        // nip33 kind with order id as identifier and order fields as tags (kind 38383 for orders)
        let event = new_order_event(keys, "", order.id.to_string(), tags)
            .map_err(|e| MostroInternalErr(ServiceError::NostrError(e.to_string())))?;

        info!("Sending replaceable event: {event:#?}");

        // We update the order with the new event_id
        order_updated.event_id = event.id.to_string();

        if let Ok(client) = get_nostr_client() {
            if client.send_event(&event).await.is_err() {
                tracing::warn!("order id : {} is expired", order_updated.id)
            }
        }
    };

    info!(
        "Order Id: {} updated Nostr new Status: {}",
        order.id,
        status.to_string()
    );

    Ok(order_updated)
}

pub async fn connect_nostr() -> Result<Client, MostroError> {
    let nostr_settings = Settings::get_nostr();

    let mut limits = RelayLimits::default();
    // Some specific events can have a bigger size than regular events
    // So we increase the limits for those events
    limits.messages.max_size = Some(6_000);
    limits.events.max_size = Some(6_500);
    let opts = ClientOptions::new().relay_limits(limits);

    // Create new client
    let client = ClientBuilder::default().opts(opts).build();

    // Add relays
    for relay in nostr_settings.relays.iter() {
        client
            .add_relay(relay)
            .await
            .map_err(|e| MostroInternalErr(ServiceError::NostrError(e.to_string())))?;
    }

    // Connect to relays and keep connection alive
    client.connect().await;

    Ok(client)
}

pub async fn show_hold_invoice(
    my_keys: &Keys,
    payment_request: Option<String>,
    buyer_pubkey: &PublicKey,
    seller_pubkey: &PublicKey,
    mut order: Order,
    request_id: Option<u64>,
) -> Result<(), MostroError> {
    let mut ln_client = lightning::LndConnector::new().await?;
    // Seller pays only the order amount and their Mostro fee
    // Dev fee is NOT charged to seller - it's paid by mostrod from its earnings
    let new_amount = order.amount + order.fee;

    // Now we generate the hold invoice that seller should pay
    let (invoice_response, preimage, hash) = ln_client
        .create_hold_invoice(
            &messages::hold_invoice_description(
                &order.id.to_string(),
                &order.fiat_code,
                &order.fiat_amount.to_string(),
            )
            .map_err(|e| MostroInternalErr(ServiceError::HoldInvoiceError(e.to_string())))?,
            new_amount,
        )
        .await
        .map_err(|e| MostroInternalErr(ServiceError::HoldInvoiceError(e.to_string())))?;
    if let Some(invoice) = payment_request {
        order.buyer_invoice = Some(invoice);
    };

    // Using CRUD to update all fiels
    order.preimage = Some(bytes_to_string(&preimage));
    order.hash = Some(bytes_to_string(&hash));
    order.status = Status::WaitingPayment.to_string();
    order.buyer_pubkey = Some(buyer_pubkey.to_string());
    order.seller_pubkey = Some(seller_pubkey.to_string());

    // We need to publish a new event with the new status
    let pool = db::connect()
        .await
        .map_err(|e| MostroInternalErr(ServiceError::DbAccessError(e.to_string())))?;
    let order_updated = update_order_event(my_keys, Status::WaitingPayment, &order)
        .await
        .map_err(|e| MostroInternalErr(ServiceError::NostrError(e.to_string())))?;
    order_updated
        .update(&pool)
        .await
        .map_err(|e| MostroInternalErr(ServiceError::DbAccessError(e.to_string())))?;

    let mut new_order = order.as_new_order();
    new_order.status = Some(Status::WaitingPayment);
    new_order.amount = new_amount;
    // Clear buyer_invoice to avoid leaking buyer's payment info to seller
    new_order.buyer_invoice = None;

    // We create a Message to send the hold invoice to seller
    enqueue_order_msg(
        request_id,
        Some(order.id),
        Action::PayInvoice,
        Some(Payload::PaymentRequest(
            Some(new_order),
            invoice_response.payment_request,
            None,
        )),
        *seller_pubkey,
        order.trade_index_seller,
    )
    .await;

    // We notify the buyer (maker) that their order was taken and seller must pay the hold invoice
    enqueue_order_msg(
        request_id,
        Some(order.id),
        Action::WaitingSellerToPay,
        None,
        *buyer_pubkey,
        order.trade_index_buyer,
    )
    .await;

    let _ = invoice_subscribe(hash, request_id).await;

    Ok(())
}

// Create function to reuse in case of resubscription
pub async fn invoice_subscribe(hash: Vec<u8>, request_id: Option<u64>) -> Result<(), MostroError> {
    let mut ln_client_invoices = lightning::LndConnector::new().await?;
    let (tx, mut rx) = channel(100);

    let invoice_task = {
        async move {
            let _ = ln_client_invoices
                .subscribe_invoice(hash, tx)
                .await
                .map_err(|e| e.to_string());
        }
    };
    tokio::spawn(invoice_task);

    // Arc clone db pool to safe use across threads
    let pool = get_db_pool();

    let subs = {
        async move {
            // Receiving msgs from the invoice subscription.
            while let Some(msg) = rx.recv().await {
                let hash = bytes_to_string(msg.hash.as_ref());
                // If this invoice was paid by the seller
                if msg.state == InvoiceState::Accepted {
                    let keys = match get_keys() {
                        Ok(k) => k,
                        Err(e) => {
                            info!("Failed to get keys: {e}");
                            continue;
                        }
                    };
                    if let Err(e) = flow::hold_invoice_paid(&hash, request_id, &pool, &keys).await {
                        info!("Invoice flow error {e}");
                    } else {
                        info!("Invoice with hash {hash} accepted!");
                    }
                } else if msg.state == InvoiceState::Settled {
                    // If the payment was settled
                    if let Err(e) = flow::hold_invoice_settlement(&hash, &pool).await {
                        info!("Invoice flow error {e}");
                    }
                } else if msg.state == InvoiceState::Canceled {
                    // If the payment was canceled
                    if let Err(e) = flow::hold_invoice_canceled(&hash, &pool).await {
                        info!("Invoice flow error {e}");
                    }
                } else {
                    info!("Invoice with hash: {hash} subscribed!");
                }
            }
        }
    };
    tokio::spawn(subs);
    Ok(())
}

pub async fn get_market_amount_and_fee(
    fiat_amount: i64,
    fiat_code: &str,
    premium: i64,
) -> Result<(i64, i64)> {
    // Update amount order
    let new_sats_amount = get_market_quote(&fiat_amount, fiat_code, premium).await?;
    let fee = get_fee(new_sats_amount);

    Ok((new_sats_amount, fee))
}

/// Set order sats amount, this used when a buyer takes a sell order
pub async fn set_waiting_invoice_status(
    order: &mut Order,
    buyer_pubkey: PublicKey,
    request_id: Option<u64>,
) -> Result<i64> {
    let kind = OrderKind::from_str(&order.kind)
        .map_err(|_| MostroCantDo(CantDoReason::InvalidOrderKind))?;
    let status = Status::WaitingBuyerInvoice;

    // Buyer receives order amount minus only the Mostro fee
    // Dev fee is NOT charged to buyer - it's paid by mostrod from its earnings
    let buyer_final_amount = order.amount.saturating_sub(order.fee);
    // We send this data related to the buyer
    let order_data = SmallOrder::new(
        Some(order.id),
        Some(kind),
        Some(status),
        buyer_final_amount,
        order.fiat_code.clone(),
        order.min_amount,
        order.max_amount,
        order.fiat_amount,
        order.payment_method.clone(),
        order.premium,
        None,
        None,
        None,
        Some(order.created_at),
        None,
    );
    // We create a Message
    enqueue_order_msg(
        request_id,
        Some(order.id),
        Action::AddInvoice,
        Some(Payload::Order(order_data)),
        buyer_pubkey,
        order.trade_index_buyer,
    )
    .await;

    // We notify the seller (maker) that their order was taken and buyer must add invoice
    let seller_pubkey = order.get_seller_pubkey().map_err(MostroInternalErr)?;
    enqueue_order_msg(
        request_id,
        Some(order.id),
        Action::WaitingBuyerInvoice,
        None,
        seller_pubkey,
        order.trade_index_seller,
    )
    .await;

    Ok(order.amount)
}

/// Send message to buyer and seller to vote for counterpart
pub async fn rate_counterpart(
    buyer_pubkey: &PublicKey,
    seller_pubkey: &PublicKey,
    order: &Order,
    request_id: Option<u64>,
) -> Result<()> {
    // Send dm to counterparts
    // to buyer
    enqueue_order_msg(
        request_id,
        Some(order.id),
        Action::Rate,
        None,
        *buyer_pubkey,
        None,
    )
    .await;
    // to seller
    enqueue_order_msg(
        request_id,
        Some(order.id),
        Action::Rate,
        None,
        *seller_pubkey,
        None,
    )
    .await;

    Ok(())
}

/// Settle a seller hold invoice
#[allow(clippy::too_many_arguments)]
pub async fn settle_seller_hold_invoice(
    event: &UnwrappedMessage,
    ln_client: &mut LndConnector,
    action: Action,
    is_admin: bool,
    order: &Order,
) -> Result<(), MostroError> {
    // Get seller pubkey
    let seller_pubkey = order
        .get_seller_pubkey()
        .map_err(|_| MostroCantDo(CantDoReason::InvalidPubkey))?
        .to_string();
    // Get sender pubkey (trade key that authored the rumor)
    let sender_pubkey = event.sender.to_string();
    // Check if the pubkey is right
    if !is_admin && sender_pubkey != seller_pubkey {
        return Err(MostroCantDo(CantDoReason::InvalidPubkey));
    }

    // Settling the hold invoice
    if let Some(preimage) = order.preimage.as_ref() {
        ln_client.settle_hold_invoice(preimage).await?;
        info!("{action}: Order Id {}: hold invoice settled", order.id);
    } else {
        return Err(MostroCantDo(CantDoReason::InvalidInvoice));
    }
    Ok(())
}

pub fn bytes_to_string(bytes: &[u8]) -> String {
    bytes.iter().fold(String::new(), |mut output, b| {
        let _ = write!(output, "{:02x}", b);
        output
    })
}

pub async fn enqueue_cant_do_msg(
    request_id: Option<u64>,
    order_id: Option<Uuid>,
    reason: CantDoReason,
    destination_key: PublicKey,
) {
    // Send message to event creator
    let message = Message::cant_do(order_id, request_id, Some(Payload::CantDo(Some(reason))));
    MESSAGE_QUEUES
        .queue_order_cantdo
        .write()
        .await
        .push((message, destination_key));
}

pub async fn enqueue_restore_session_msg(payload: Option<Payload>, destination_key: PublicKey) {
    // Send message to event creator
    let message = Message::new_restore(payload);
    MESSAGE_QUEUES
        .queue_restore_session_msg
        .write()
        .await
        .push((message, destination_key));
}

pub async fn enqueue_order_msg(
    request_id: Option<u64>,
    order_id: Option<Uuid>,
    action: Action,
    payload: Option<Payload>,
    destination_key: PublicKey,
    trade_index: Option<i64>,
) {
    // Send message to event creator
    let message = Message::new_order(order_id, request_id, trade_index, action, payload);
    MESSAGE_QUEUES
        .queue_order_msg
        .write()
        .await
        .push((message, destination_key));
}

pub fn get_fiat_amount_requested(order: &Order, msg: &Message) -> Option<i64> {
    // Check if order is range and get amount request after checking boundaries
    // set order fiat amount to the value requested preparing for hold invoice
    if order.is_range_order() {
        if let Some(amount_buyer) = msg.get_inner_message_kind().get_amount() {
            info!("amount_buyer: {amount_buyer}");
            match Some(amount_buyer) <= order.max_amount && Some(amount_buyer) >= order.min_amount {
                true => Some(amount_buyer),
                false => None,
            }
        } else {
            None
        }
    } else {
        // If order is not a range order return an Option with fiat amount of the order
        Some(order.fiat_amount)
    }
}

/// Getter function with error management for nostr Client
pub fn get_nostr_client() -> Result<&'static Client, MostroError> {
    if let Some(client) = NOSTR_CLIENT.get() {
        Ok(client)
    } else {
        Err(MostroInternalErr(ServiceError::NostrError(
            "Client not initialized!".to_string(),
        )))
    }
}

/// Getter function with error management for nostr relays
pub async fn get_nostr_relays() -> Option<HashMap<RelayUrl, Relay>> {
    if let Some(client) = NOSTR_CLIENT.get() {
        Some(client.relays().await)
    } else {
        None
    }
}

pub async fn get_dispute(msg: &Message, pool: &Pool<Sqlite>) -> Result<Dispute, MostroError> {
    let dispute_msg = msg.get_inner_message_kind();
    let dispute_id = dispute_msg
        .id
        .ok_or(MostroInternalErr(ServiceError::InvalidDisputeId))?;
    let dispute = Dispute::by_id(pool, dispute_id)
        .await
        .map_err(|e| MostroInternalErr(ServiceError::DbAccessError(e.to_string())))?;
    if let Some(dispute) = dispute {
        Ok(dispute)
    } else {
        Err(MostroInternalErr(ServiceError::InvalidDisputeId))
    }
}

pub async fn get_order(msg: &Message, pool: &Pool<Sqlite>) -> Result<Order, MostroError> {
    let order_msg = msg.get_inner_message_kind();
    let order_id = order_msg
        .id
        .ok_or(MostroInternalErr(ServiceError::InvalidOrderId))?;
    let order = Order::by_id(pool, order_id)
        .await
        .map_err(|e| MostroInternalErr(ServiceError::DbAccessError(e.to_string())))?;
    if let Some(order) = order {
        Ok(order)
    } else {
        Err(MostroInternalErr(ServiceError::InvalidOrderId))
    }
}

/// Efficiently retrieves multiple orders by their IDs for a specific user
///
/// # Arguments
/// * `pool` - Database connection pool
/// * `orders` - Vector of order IDs as UUIDs
/// * `user_pubkey` - Public key of the user requesting the orders
///
/// # Returns
/// * `Result<Vec<Order>, MostroError>` - Vector of found orders that belong to the user, empty if no orders found or input is empty
///
/// # Behavior
/// - Returns empty vector if input `orders` is empty
/// - Returns only the orders that exist in the database AND belong to the user (as buyer or seller)
/// - Uses a single SQL query with IN clause and user validation for efficiency
/// - Validates that the user has access to the requested orders
pub async fn get_user_orders_by_id(
    pool: &Pool<Sqlite>,
    orders: &[Uuid],
    user_pubkey: &str,
) -> Result<Vec<Order>, MostroError> {
    // Return empty vector if no orders requested
    if orders.is_empty() {
        return Ok(Vec::new());
    }

    let mut query_builder = QueryBuilder::new("SELECT * FROM orders WHERE id IN (");

    {
        let mut separated = query_builder.separated(", ");
        for order_id in orders {
            separated.push_bind(order_id);
        }
    }

    query_builder.push(") AND (");
    query_builder.push("master_buyer_pubkey = ");
    query_builder.push_bind(user_pubkey);
    query_builder.push(" OR master_seller_pubkey = ");
    query_builder.push_bind(user_pubkey);
    query_builder.push(")");

    // Preserve the caller requested order sequence so that response payload matches
    query_builder.push(" ORDER BY CASE id");
    for (index, order_id) in orders.iter().enumerate() {
        query_builder.push(" WHEN ");
        query_builder.push_bind(order_id);
        query_builder.push(" THEN ");
        query_builder.push_bind(index as i64);
    }
    query_builder.push(" END");

    let found_orders = query_builder
        .build_query_as::<Order>()
        .fetch_all(pool)
        .await
        .map_err(|e| MostroInternalErr(ServiceError::DbAccessError(e.to_string())))?;

    Ok(found_orders)
}

pub async fn validate_invoice(msg: &Message, order: &Order) -> Result<Option<String>, MostroError> {
    // init payment request to None
    let mut payment_request = None;
    // if payment request is present
    if let Some(pr) = msg.get_inner_message_kind().get_payment_request() {
        // Calculate total buyer fees (only Mostro fee)
        // Dev fee is NOT charged to buyer - it's paid by mostrod from its earnings
        let total_buyer_fees = order.fee;

        // if invoice is valid
        if is_valid_invoice(
            pr.clone(),
            Some(order.amount as u64),
            Some(total_buyer_fees as u64),
        )
        .await
        .is_err()
        {
            return Err(MostroCantDo(CantDoReason::InvalidInvoice));
        }
        // if invoice is valid return it
        else {
            payment_request = Some(pr);
        }
    }
    Ok(payment_request)
}

pub async fn notify_taker_reputation(
    pool: &Pool<Sqlite>,
    order: &Order,
) -> Result<(), MostroError> {
    // Check if is buy or sell order we need this info to understand the user needed and the receiver of notification
    let is_buy_order = order.is_buy_order().is_ok();
    // Get user needed
    let user = match is_buy_order {
        true => order.master_seller_pubkey.clone(),
        false => order.master_buyer_pubkey.clone(),
    };

    let master_key = match user {
        Some(user) => user.to_string(),
        None => return Err(MostroCantDo(CantDoReason::InvalidPubkey)),
    };

    let reputation_data = match is_user_present(pool, master_key).await {
        Ok(user) => {
            let now = Timestamp::now().as_secs();
            UserInfo {
                rating: user.total_rating,
                reviews: user.total_reviews,
                operating_days: (now - user.created_at as u64) / 86400,
            }
        }
        Err(_) => UserInfo {
            rating: 0.0,
            reviews: 0,
            operating_days: 0,
        },
    };

    // Get order status
    let order_status = order.get_order_status().map_err(MostroInternalErr)?;

    // Get action for info message and receiver key
    let (action, receiver) = match order_status {
        Status::WaitingBuyerInvoice => {
            if !is_buy_order {
                (
                    Action::PayInvoice,
                    order.get_seller_pubkey().map_err(MostroInternalErr)?,
                )
            } else {
                //FIX for the case of a buy order and maker is adding invoice
                // just return ok
                return Ok(());
            }
        }
        Status::WaitingPayment => {
            if is_buy_order {
                (
                    Action::AddInvoice,
                    order.get_buyer_pubkey().map_err(MostroInternalErr)?,
                )
            } else {
                return Err(MostroCantDo(CantDoReason::NotAllowedByStatus));
            }
        }
        _ => {
            return Err(MostroCantDo(CantDoReason::NotAllowedByStatus));
        }
    };

    enqueue_order_msg(
        None,
        Some(order.id),
        action,
        Some(Payload::Peer(Peer {
            pubkey: "".to_string(),
            reputation: Some(reputation_data),
        })),
        receiver,
        None,
    )
    .await;
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::bitcoin_price::BitcoinPriceManager;
    use mostro_core::message::{Message, MessageKind};
    use mostro_core::order::Order;
    use sqlx::sqlite::SqlitePoolOptions;
    use sqlx::SqlitePool;
    use std::sync::Once;
    use uuid::{uuid, Uuid};
    // Setup function to initialize common settings or data before tests
    static INIT: Once = Once::new();

    fn initialize() {
        INIT.call_once(|| {
            // Any initialization code goes here
        });
    }

    #[test]
    fn select_yadio_base_url_prefers_enabled_provider() {
        // Enabled provider with a usable URL wins over the legacy key.
        assert_eq!(
            select_yadio_base_url(
                Some(("https://provider.example", true)),
                "https://legacy.example"
            ),
            "https://provider.example"
        );
    }

    #[test]
    fn select_yadio_base_url_strips_trailing_slash_like_provider_new() {
        // A configured trailing slash must not survive — otherwise appending
        // `/convert` yields `//convert`. Matches `YadioProvider::new`.
        assert_eq!(
            select_yadio_base_url(
                Some(("https://api.yadio.io/", true)),
                "https://legacy.example"
            ),
            "https://api.yadio.io"
        );
        // Whitespace + multiple trailing slashes both normalized away.
        assert_eq!(
            select_yadio_base_url(Some(("  https://api.yadio.io//  ", true)), "ignored"),
            "https://api.yadio.io"
        );
        // The legacy fallback is normalized the same way.
        assert_eq!(
            select_yadio_base_url(None, "https://legacy.example/"),
            "https://legacy.example"
        );
    }

    #[test]
    fn select_yadio_base_url_falls_back_when_provider_unusable() {
        let legacy = "https://legacy.example";
        // Disabled provider → fall back to legacy even with a URL set.
        assert_eq!(
            select_yadio_base_url(Some(("https://provider.example", false)), legacy),
            legacy
        );
        // Enabled but blank / slash-only URL → fall back to legacy.
        assert_eq!(select_yadio_base_url(Some(("   ", true)), legacy), legacy);
        assert_eq!(select_yadio_base_url(Some(("/", true)), legacy), legacy);
        // No provider entry at all → legacy.
        assert_eq!(select_yadio_base_url(None, legacy), legacy);
    }

    async fn setup_orders_pool() -> SqlitePool {
        let pool = SqlitePoolOptions::new()
            .max_connections(1)
            .connect(":memory:")
            .await
            .unwrap();

        sqlx::query(include_str!("../migrations/20221222153301_orders.sql"))
            .execute(&pool)
            .await
            .unwrap();
        sqlx::query(include_str!("../migrations/20251126120000_dev_fee.sql"))
            .execute(&pool)
            .await
            .unwrap();
        sqlx::query(include_str!(
            "../migrations/20260530120000_cashu_escrow_fields.sql"
        ))
        .execute(&pool)
        .await
        .unwrap();

        pool
    }

    async fn insert_order(
        pool: &SqlitePool,
        id: Uuid,
        identity_buyer_pubkey: Option<&str>,
        identity_seller_pubkey: Option<&str>,
        creator_pubkey: &str,
    ) {
        sqlx::query(
            r#"
            INSERT INTO orders (
                id,
                kind,
                event_id,
                creator_pubkey,
                status,
                premium,
                payment_method,
                amount,
                fiat_code,
                fiat_amount,
                created_at,
                expires_at,
                master_buyer_pubkey,
                master_seller_pubkey
            ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
        "#,
        )
        .bind(id)
        .bind("buy")
        .bind(id.simple().to_string())
        .bind(creator_pubkey)
        .bind("active")
        .bind(0_i64)
        .bind("ln")
        .bind(1_000_i64)
        .bind("USD")
        .bind(1_000_i64)
        .bind(1_000_i64)
        .bind(2_000_i64)
        .bind(identity_buyer_pubkey)
        .bind(identity_seller_pubkey)
        .execute(pool)
        .await
        .unwrap();
    }

    #[test]
    fn test_bytes_to_string() {
        initialize();
        let bytes = vec![0xde, 0xad, 0xbe, 0xef];
        let result = bytes_to_string(&bytes);
        assert_eq!(result, "deadbeef");
    }

    #[tokio::test]
    async fn test_get_market_quote_url_construction() {
        initialize();
        // Test the URL construction logic without making actual API calls
        // This test verifies that the API URL format is correct
        let base_url = "https://api.yadio.io";
        let fiat_amount = 1000;
        let fiat_code = "USD";

        let expected_url = format!("{}/convert/{}/{}/BTC", base_url, fiat_amount, fiat_code);
        assert_eq!(expected_url, "https://api.yadio.io/convert/1000/USD/BTC");

        // Test currency list URL construction
        let currencies_url = format!("{}/currencies", base_url);
        assert_eq!(currencies_url, "https://api.yadio.io/currencies");
    }

    #[tokio::test]
    async fn test_get_nostr_client_failure() {
        initialize();
        // Ensure NOSTR_CLIENT is not initialized for the test
        let client = NOSTR_CLIENT.get();
        assert!(client.is_none());
    }

    #[tokio::test]
    async fn test_get_nostr_client_success() {
        initialize();
        // Mock NOSTR_CLIENT initialization
        let client = Client::default();
        NOSTR_CLIENT.set(client).unwrap();
        let client_result = get_nostr_client();
        assert!(client_result.is_ok());
    }

    #[test]
    fn test_bytes_to_string_empty() {
        initialize();
        let bytes: Vec<u8> = vec![];
        let result = bytes_to_string(&bytes);
        assert_eq!(result, "");
    }

    #[tokio::test]
    async fn test_send_dm() {
        initialize();
        // Mock the send_dm function
        let receiver_pubkey = Keys::generate().public_key();
        let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
        let message = Message::Order(MessageKind::new(
            Some(uuid),
            None,
            None,
            Action::FiatSent,
            None,
        ));
        let payload = message.as_json().unwrap();
        let sender_keys = Keys::generate();
        // Now error is well manager this call will fail now, previously test was ok becuse error was not managed
        // now just make it ok and then will make a better test
        let result = send_dm(receiver_pubkey, &sender_keys, &payload, None).await;
        assert!(result.is_err());
    }

    #[tokio::test]
    async fn test_get_fiat_amount_requested() {
        initialize();
        let uuid = uuid!("308e1272-d5f4-47e6-bd97-3504baea9c23");
        let order = Order {
            amount: 1000,
            min_amount: Some(500),
            max_amount: Some(2000),
            ..Default::default()
        };
        let message = Message::Order(MessageKind::new(
            Some(uuid),
            Some(1),
            Some(1),
            Action::TakeSell,
            Some(Payload::Amount(order.amount)),
        ));
        let amount = get_fiat_amount_requested(&order, &message);
        assert_eq!(amount, Some(1000));
    }

    #[tokio::test]
    async fn test_get_user_orders_by_id_filters_and_preserves_order() {
        initialize();
        let pool = setup_orders_pool().await;
        let user_pubkey = "a".repeat(64);
        let other_pubkey = "b".repeat(64);

        let first_id = Uuid::new_v4();
        let second_id = Uuid::new_v4();
        let third_id = Uuid::new_v4();

        insert_order(
            &pool,
            first_id,
            Some(&user_pubkey),
            Some(&other_pubkey),
            &user_pubkey,
        )
        .await;
        insert_order(
            &pool,
            second_id,
            Some(&other_pubkey),
            Some(&user_pubkey),
            &user_pubkey,
        )
        .await;
        insert_order(
            &pool,
            third_id,
            Some(&other_pubkey),
            Some(&other_pubkey),
            &other_pubkey,
        )
        .await;

        let requested = vec![second_id, first_id, third_id];

        let orders = get_user_orders_by_id(&pool, &requested, &user_pubkey)
            .await
            .unwrap();

        assert_eq!(orders.len(), 2);
        assert_eq!(orders[0].id, second_id);
        assert_eq!(orders[1].id, first_id);
    }

    #[tokio::test]
    async fn test_get_user_orders_by_id_empty_input() {
        initialize();
        let pool = setup_orders_pool().await;
        let user_pubkey = "a".repeat(64);

        let orders = get_user_orders_by_id(&pool, &[], &user_pubkey)
            .await
            .unwrap();

        assert!(orders.is_empty());
    }

    #[test]
    fn test_get_dev_fee_basic() {
        // 1000 sats Mostro fee at 30% -> 300 sats
        let fee = calculate_dev_fee(1_000, 0.30);
        assert_eq!(fee, 300);
    }

    #[test]
    fn test_get_dev_fee_rounding() {
        // 333 * 0.30 = 99.9 -> rounds to 100
        let fee = calculate_dev_fee(333, 0.30);
        assert_eq!(fee, 100);
    }

    #[test]
    fn test_get_dev_fee_zero() {
        let fee = calculate_dev_fee(0, 0.30);
        assert_eq!(fee, 0);
    }

    #[test]
    fn test_get_dev_fee_tiny_amounts() {
        // With 30%, 1 * 0.30 = 0.3 -> 0
        let fee = calculate_dev_fee(1, 0.30);
        assert_eq!(fee, 0);
    }

    #[test]
    fn maker_bond_notional_uses_fixed_amount_directly() {
        // Phase 5: a fixed-price order carries its sats `amount`, so the
        // maker-bond notional is exactly that — no price lookup, no API
        // dependency in this path.
        let order = Order {
            amount: 50_000,
            fiat_code: "USD".to_string(),
            fiat_amount: 25,
            ..Default::default()
        };
        assert_eq!(maker_bond_notional_sats(&order).unwrap(), 50_000);
    }

    #[test]
    fn maker_bond_notional_range_sizes_against_max_at_price() {
        // Phase 6: a range order sizes the notional against `max_amount`
        // converted at the cached price: 100 fiat / 60_000 * 1e8 ≈ 166_666
        // sats. Unique fiat_code avoids clobbering the shared price cache.
        BitcoinPriceManager::set_price_for_test("T6RANGE", 60_000.0);
        let order = Order {
            amount: 0,
            min_amount: Some(10),
            max_amount: Some(100),
            fiat_code: "T6RANGE".to_string(),
            fiat_amount: 0,
            ..Default::default()
        };
        assert!(order.is_range_order());
        assert_eq!(maker_bond_notional_sats(&order).unwrap(), 166_666);
    }

    #[test]
    fn maker_bond_notional_range_rejects_non_positive_max() {
        // `is_range_order()` only checks `Some`-ness, so a `max_amount` of 0
        // still enters the range branch and must be rejected before bond
        // sizing (it would divide-by-zero in the proportional slash). A
        // `None` max can't reach here — `is_range_order()` would be false.
        let order = Order {
            amount: 0,
            min_amount: Some(10),
            max_amount: Some(0),
            fiat_code: "T6ZERO".to_string(),
            fiat_amount: 0,
            ..Default::default()
        };
        assert!(order.is_range_order());
        let err = maker_bond_notional_sats(&order).unwrap_err();
        assert!(
            matches!(err, MostroInternalErr(ServiceError::UnexpectedError(_))),
            "expected UnexpectedError for non-positive max_amount, got {err:?}"
        );
    }
}
