/*-------------------------------------------------------------------------
 * Copyright (c) Microsoft Corporation.  All rights reserved.
 *
 * documentdb_gateway_core/src/context/connection.rs
 *
 *-------------------------------------------------------------------------
 */

use std::{
    hash::{DefaultHasher, Hash, Hasher},
    sync::Arc,
};

use bson::RawDocumentBuf;
use openssl::ssl::SslRef;
use tokio::time::{Duration, Instant};
use uuid::{Builder, Uuid};

use crate::{
    auth::AuthState,
    configuration::DynamicConfiguration,
    context::{
        session::SessionKey, Cursor, CursorKey, CursorRef, CursorStoreEntry, LogicalSessionId,
        ServiceContext, TransactionNumber,
    },
    error::Result,
    postgres::conn_mgmt::Connection,
    security::principal::Principal,
    telemetry::TelemetryProvider,
};

#[derive(Debug)]
pub struct ConnectionContext {
    pub start_time: Instant,
    pub connection_id: Uuid,
    pub service_context: Arc<ServiceContext>,
    pub auth_state: AuthState,
    pub requires_response: bool,
    pub client_information: Option<RawDocumentBuf>,
    pub transaction: Option<(LogicalSessionId, TransactionNumber)>,
    pub telemetry_provider: Option<Box<dyn TelemetryProvider>>,
    pub ip_address: String,
    pub cipher_type: i32,
    pub ssl_protocol: String,
    transport_protocol: String,
    connection_id_hash: i32,
}

impl ConnectionContext {
    #[must_use]
    pub fn new(
        service_context: ServiceContext,
        telemetry_provider: Option<Box<dyn TelemetryProvider>>,
        ip_address: String,
        tls_config: Option<&SslRef>,
        connection_id: Uuid,
        transport_protocol: String,
    ) -> Self {
        let cipher_type = if let Some(tls) = tls_config {
            service_context
                .tls_provider()
                .ciphersuite_to_i32(tls.current_cipher())
        } else {
            0
        };

        let ssl_protocol = tls_config
            .map(|tls| tls.version_str().to_owned())
            .unwrap_or_default();

        Self {
            start_time: Instant::now(),
            connection_id,
            service_context: Arc::new(service_context),
            auth_state: AuthState::new(),
            requires_response: true,
            client_information: None,
            transaction: None,
            telemetry_provider,
            ip_address,
            cipher_type,
            ssl_protocol,
            transport_protocol,
            connection_id_hash: Self::get_uuid_hash(connection_id),
        }
    }

    #[must_use]
    pub fn get_cursor(&self, id: i64, caller: &Principal) -> Option<CursorStoreEntry> {
        let key = CursorKey::new(id.into(), caller.clone());

        // If there is a transaction, validate that the transaction owns the cursor before using it
        if let Some((lsid, _)) = self.transaction.as_ref() {
            let transaction_store = self.service_context.transaction_store();
            let session_key = SessionKey::new(lsid.clone(), caller.clone());
            if let Some(entry) = transaction_store.transactions.get(&session_key) {
                let (_, transaction) = entry.value();
                if !transaction.contains_cursor(id.into()) {
                    return None;
                }
            }
        }

        self.service_context.cursor_store().get_cursor(&key)
    }

    #[must_use]
    pub fn get_cursor_ref(&self, id: i64, caller: &Principal) -> Option<CursorRef> {
        let key = CursorKey::new(id.into(), caller.clone());
        self.service_context.cursor_store().get_cursor_ref(&key)
    }

    #[expect(
        clippy::too_many_arguments,
        reason = "cursor creation requires multiple parameters"
    )]
    pub fn add_cursor(
        &self,
        conn: Option<Arc<Connection>>,
        cursor: Cursor,
        db: &str,
        collection: &str,
        cursor_timeout: Duration,
        lsid: Option<LogicalSessionId>,
        transaction_number: Option<TransactionNumber>,
        caller: &Principal,
    ) {
        let key = CursorKey::new(cursor.cursor_id, caller.clone());

        let value = CursorStoreEntry {
            conn,
            cursor,
            db: db.to_owned(),
            collection: collection.to_owned(),
            timestamp: Instant::now(),
            cursor_timeout,
            lsid,
            transaction_number,
        };

        // If there is a transaction, add it to the tracked cursors
        if let Some((lsid, _)) = self.transaction.as_ref() {
            let transaction_store = self.service_context.transaction_store();
            let session_key: crate::context::StoreKey<LogicalSessionId> =
                SessionKey::new(lsid.clone(), caller.clone());
            if let Some(entry) = transaction_store.transactions.get(&session_key) {
                let (_, transaction) = entry.value();
                transaction.add_cursor(*key.id());
            }
        }

        // Otherwise add it to the service context
        self.service_context.cursor_store().add_cursor(key, value);
    }

    /// # Errors
    ///
    /// Returns an error if the operation fails.
    pub fn allocate_data_pool(&self, password: &str) -> Result<()> {
        let username = self.auth_state.username()?;

        self.service_context
            .connection_pool_manager()
            .allocate_data_pool(
                username,
                password,
                self.service_context.dynamic_configuration().as_ref(),
            )
    }

    #[must_use]
    pub fn dynamic_configuration(&self) -> Arc<dyn DynamicConfiguration> {
        self.service_context.dynamic_configuration()
    }

    /// Generates a per-request activity ID by embedding the given `request_id`
    /// into the caller’s connection UUID and returning it as a hyphenated string.
    ///
    /// The function copies the current `connection_id` (a 16-byte UUID), overwrites
    /// bytes 12..16 (the final 4 bytes) with `request_id.to_be_bytes()`
    /// to preserve UUID version/variant bits, then returns the resulting UUID’s
    /// canonical (lowercase, hyphenated) string form.
    ///
    /// # Parameters
    /// - `request_id`: 32-bit identifier to embed (stored big-endian in bytes 12–15).
    ///
    /// # Returns
    /// A `String` containing the new UUID, e.g. `"550e8400-e29b-41d4-a716-446655440000"`.
    #[must_use]
    pub fn generate_request_activity_id(&self, request_id: i32) -> String {
        let mut activity_id_bytes = *self.connection_id.as_bytes();
        activity_id_bytes[12..].copy_from_slice(&request_id.to_be_bytes());
        Builder::from_bytes(activity_id_bytes)
            .into_uuid()
            .to_string()
    }

    #[must_use]
    pub fn transport_protocol(&self) -> &str {
        &self.transport_protocol
    }

    /// Returns `true` if request metrics are enabled for this connection.
    /// This is a temporary measure until we have a more comprehensive metrics system in place.
    #[must_use]
    pub fn request_metrics_enabled(&self) -> bool {
        self.service_context.request_metrics_enabled()
    }

    #[must_use]
    pub const fn get_connection_id_hash(&self) -> i32 {
        self.connection_id_hash
    }

    /// Returns a non-negative 32-bit hash for `self.connection_id`.
    ///
    /// Implementation details:
    /// - Hashes `connection_id` with `DefaultHasher` to a 64-bit value.
    /// - Folds to 32 bits by `XORing` high and low halves.
    /// - Masks off the sign bit (`& 0x7fff_ffff`) so the result fits in `0..=i31::MAX`.
    fn get_uuid_hash(connection_id: Uuid) -> i32 {
        let mut hasher = DefaultHasher::new();
        connection_id.hash(&mut hasher);
        let finished_hash = hasher.finish();
        ((finished_hash ^ (finished_hash >> 32)) & 0x7fff_ffff) as i32
    }
}
