// Copyright 2025 Georges Racinet <georges.racinet@cloudcrane.io>
//
// This software may be used and distributed according to the terms of the
// GNU General Public License version 2 or any later version.
// SPDX-License-Identifier: GPL-2.0-or-later
use std::fmt::{Debug, Formatter};
use std::num::NonZero;
use std::sync::Arc;
use tokio::sync::RwLock;
use tokio_util::sync::CancellationToken;
use tonic::{
    metadata::Ascii, metadata::MetadataMap, metadata::MetadataValue, Extensions, Request, Response,
    Status,
};
use tracing::{info, instrument};

use lru::LruCache;

use crate::config::Config;
use crate::gitaly::Repository;
use crate::gitlab::revision::gitlab_revision_node_prefix;
use crate::hgitaly::mercurial_operations_service_client::MercurialOperationsServiceClient;
use crate::hgitaly::mercurial_operations_service_server::{
    MercurialOperationsService, MercurialOperationsServiceServer,
};
use crate::hgitaly::{
    InvalidateMergeAnalysisRequest, InvalidateMergeAnalysisResponse, MergeAnalysisRequest,
    MergeAnalysisResponse,
};
use crate::metadata::correlation_id;
use crate::repository::RequestWithRepo;
use crate::repository::{default_repo_spec_error_status, repo_store_vfs};
use crate::sidecar;
use crate::util::tracing_span_id;

// Should be made configurable.
// Objects are reasonably small so we can keep quite a few.
const MERGE_ANALYSIS_CACHE_CAPACITY: NonZero<usize> = unsafe { NonZero::new_unchecked(500) };

/// The cache key is made of:
///
/// - repository's `gl_repository` identifier (e.g., `project-123`)
/// - source changeset node id.
/// - target changeset node id,
/// - skip_conflicts
type MergeAnalysisCacheKey = (String, String, String, bool);

type MergeAnalysisCacheVal = Arc<RwLock<Option<Result<MergeAnalysisResponse, Status>>>>;

#[derive(Debug)]
#[allow(dead_code)]
pub struct MercurialOperationsServiceImpl {
    config: Arc<Config>,
    shutdown_token: CancellationToken,
    sidecar_servers: Arc<sidecar::Servers>,
    merge_analysis_cache: RwLock<LruCache<MergeAnalysisCacheKey, MergeAnalysisCacheVal>>,
}

#[tonic::async_trait]
impl MercurialOperationsService for MercurialOperationsServiceImpl {
    async fn merge_analysis(
        &self,
        request: Request<MergeAnalysisRequest>,
    ) -> Result<Response<MergeAnalysisResponse>, Status> {
        let (metadata, ext, inner) = request.into_parts();

        self.inner_merge_analysis(inner, correlation_id(&metadata), &metadata, ext)
            .await
    }

    async fn invalidate_merge_analysis(
        &self,
        request: Request<InvalidateMergeAnalysisRequest>,
    ) -> Result<Response<InvalidateMergeAnalysisResponse>, Status> {
        let (metadata, _ext, inner) = request.into_parts();

        self.inner_invalidate_merge_analysis(inner, correlation_id(&metadata))
            .await
    }
}

impl MercurialOperationsServiceImpl {
    #[instrument(name = "merge_analysis", skip(self, request, metadata, extensions))]
    async fn inner_merge_analysis(
        &self,
        request: MergeAnalysisRequest,
        correlation_id: Option<&MetadataValue<Ascii>>,
        metadata: &MetadataMap,
        extensions: Extensions,
    ) -> Result<Response<MergeAnalysisResponse>, Status> {
        tracing_span_id!();
        info!(
            "Processing, request={:?}",
            MergeAnalysisTracingRequest(&request)
        );
        let config = self.config.clone();
        let store_vfs = repo_store_vfs(&config, &request.repository)
            .await
            .map_err(default_repo_spec_error_status)?;

        let source_hex = format!(
            "{:x}",
            gitlab_revision_node_prefix(&store_vfs, &request.source_revision)
                .await
                .map_err(|e| Status::internal(format!("Error resolving revision: {:?}", e)))?
                .ok_or_else(|| Status::invalid_argument(format!(
                    "Source revision b'{}' not found", // b'' for unshameful comparison with Python
                    &String::from_utf8_lossy(&request.source_revision)
                )))?
        );

        let target_hex = format!(
            "{:x}",
            gitlab_revision_node_prefix(&store_vfs, &request.target_revision)
                .await
                .map_err(|e| Status::internal(format!("Error resolving revision: {:?}", e)))?
                .ok_or_else(|| Status::invalid_argument(format!(
                    "Target revision b'{}' not found",
                    &String::from_utf8_lossy(&request.target_revision)
                )))?
        );
        let skip_conflicts = request.skip_conflicts_check;
        let cache_key = (
            request
                .repository
                .as_ref()
                .expect("repository field should have been checked already not to be empty")
                .gl_repository
                .clone(),
            source_hex.clone(),
            target_hex.clone(),
            skip_conflicts,
        );

        // We keep the outer lock of the whole cache only long enough
        // to retrieve or create the inner lock
        let resp_lock = {
            let mut cache = self.merge_analysis_cache.write().await;
            cache
                .get_or_insert(cache_key, || Arc::new(RwLock::new(None)))
                .clone()
        };

        let mut cached = resp_lock.write().await;
        if let Some(resp) = cached.as_ref() {
            return resp.clone().map(Response::new);
        } else {
            // listing all fields instead of clone + mutation because we want the build failure
            // to warn us of new fields to consider adding them to the cache key.
            let pyreq = MergeAnalysisRequest {
                repository: request.repository,
                source_revision: source_hex.into_bytes(),
                target_revision: target_hex.into_bytes(),
                skip_conflicts_check: skip_conflicts,
            };
            let wrapped = Request::from_parts(metadata.clone(), extensions, pyreq);
            // sidecar will log, hence we do not need to log the cache miss.
            let pyresp = sidecar::unary!(
                self,
                wrapped,
                MercurialOperationsServiceClient,
                merge_analysis
            );
            *cached = Some(
                pyresp
                    .as_ref()
                    .map(|r| r.get_ref().clone())
                    .map_err(|e| e.clone()),
            );
            pyresp // will have trailing metadata etc.
        }
    }

    #[instrument(name = "invalidate_merge_analysis", skip(self, request))]
    async fn inner_invalidate_merge_analysis(
        &self,
        request: InvalidateMergeAnalysisRequest,
        correlation_id: Option<&MetadataValue<Ascii>>,
    ) -> Result<Response<InvalidateMergeAnalysisResponse>, Status> {
        tracing_span_id!();
        info!(
            "Processing, request={:?}",
            InvalidateMergeAnalysisTracingRequest(&request)
        );
        let gl_repo = &request
            .repository
            .ok_or_else(|| Status::invalid_argument("Blank repository in request"))?
            .gl_repository;
        let mut to_invalidate = Vec::new();
        {
            let mut cache = self.merge_analysis_cache.write().await;
            for (key, entry_arc) in cache.iter_mut() {
                if key.0 == *gl_repo {
                    to_invalidate.push(entry_arc.clone())
                }
            }
        }
        let invalidated_count: u32 = to_invalidate.len() as u32;
        for entry in to_invalidate {
            let mut unlocked = entry.write().await;
            *unlocked = None;
        }
        Ok(Response::new(InvalidateMergeAnalysisResponse {
            invalidated_count,
        }))
    }
}

impl RequestWithRepo for MergeAnalysisRequest {
    fn repository_ref(&self) -> Option<&Repository> {
        self.repository.as_ref()
    }
}

struct MergeAnalysisTracingRequest<'a>(&'a MergeAnalysisRequest);

impl Debug for MergeAnalysisTracingRequest<'_> {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("MergeAnalysisRequest")
            .field("repository", &self.0.repository)
            .field(
                "source_revision",
                &String::from_utf8_lossy(&self.0.source_revision),
            )
            .field(
                "target_revision",
                &String::from_utf8_lossy(&self.0.target_revision),
            )
            .finish()
    }
}

/// Takes care of boilerplate that would instead be in the startup sequence.
struct InvalidateMergeAnalysisTracingRequest<'a>(&'a InvalidateMergeAnalysisRequest);

impl Debug for InvalidateMergeAnalysisTracingRequest<'_> {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("MergeAnalysisRequest")
            .field("repository", &self.0.repository)
            .finish()
    }
}

/// Takes care of boilerplate that would instead be in the startup sequence.
pub fn mercurial_operations_server(
    config: &Arc<Config>,
    shutdown_token: &CancellationToken,
    sidecar_servers: &Arc<sidecar::Servers>,
) -> MercurialOperationsServiceServer<MercurialOperationsServiceImpl> {
    MercurialOperationsServiceServer::new(MercurialOperationsServiceImpl {
        config: config.clone(),
        shutdown_token: shutdown_token.clone(),
        sidecar_servers: sidecar_servers.clone(),
        merge_analysis_cache: RwLock::new(LruCache::new(MERGE_ANALYSIS_CACHE_CAPACITY)),
    })
}
