use std::collections::HashMap;
use std::sync::Arc;

use apigateway_api::proto::{
    CreditCommitRequest, CreditCommitResponse, CreditReserveRequest, CreditReserveResponse,
    CreditRollbackRequest, CreditRollbackResponse,
};
use apigateway_api::GatewayCreditService;
use creditservice_proto::credit_service_server::CreditService;
use creditservice_proto::{
    CommitReservationRequest, ReleaseReservationRequest, ReserveCreditsRequest,
};
use iam_types::PrincipalKind;
use photon_auth_client::TenantContext;
use tokio::sync::RwLock;
use tonic::{Code, Request, Response, Status};

use crate::credit_service::CreditServiceImpl;

pub struct GatewayCreditServiceImpl {
    credit_service: Arc<CreditServiceImpl>,
    reservation_contexts: Arc<RwLock<HashMap<String, TenantContext>>>,
}

impl GatewayCreditServiceImpl {
    pub fn new(credit_service: Arc<CreditServiceImpl>) -> Self {
        Self {
            credit_service,
            reservation_contexts: Arc::new(RwLock::new(HashMap::new())),
        }
    }
}

#[tonic::async_trait]
impl GatewayCreditService for GatewayCreditServiceImpl {
    async fn reserve(
        &self,
        request: Request<CreditReserveRequest>,
    ) -> Result<Response<CreditReserveResponse>, Status> {
        let req = request.into_inner();

        if req.org_id.trim().is_empty() {
            return Ok(Response::new(deny_response(
                "org_id required for credit reservation",
            )));
        }

        if req.project_id.trim().is_empty() {
            return Ok(Response::new(deny_response(
                "project_id required for credit reservation",
            )));
        }

        if req.units == 0 {
            return Err(Status::invalid_argument("units must be positive"));
        }

        let amount = i64::try_from(req.units)
            .map_err(|_| Status::invalid_argument("units exceeds i64 range"))?;
        let description = reservation_description(&req);
        let resource_type = reservation_resource_type(&req);
        let ttl_seconds = reservation_ttl(&req.attributes);

        let tenant = tenant_context_from_reserve(&req)?;
        let reserve_request = ReserveCreditsRequest {
            project_id: req.project_id.clone(),
            amount,
            description,
            resource_type,
            ttl_seconds,
        };
        let mut reserve_request = Request::new(reserve_request);
        reserve_request.extensions_mut().insert(tenant.clone());

        match self.credit_service.reserve_credits(reserve_request).await {
            Ok(response) => {
                let response = response.into_inner();
                let reservation = response
                    .reservation
                    .ok_or_else(|| Status::internal("credit reservation missing from response"))?;
                if !reservation.id.is_empty() {
                    let mut guard = self.reservation_contexts.write().await;
                    guard.insert(reservation.id.clone(), tenant);
                }
                Ok(Response::new(CreditReserveResponse {
                    allow: true,
                    reservation_id: reservation.id,
                    reason: String::new(),
                    remaining: 0,
                }))
            }
            Err(status) => match status.code() {
                Code::NotFound | Code::FailedPrecondition => {
                    Ok(Response::new(deny_response(status.message())))
                }
                Code::InvalidArgument => Err(Status::invalid_argument(status.message())),
                _ => Err(status),
            },
        }
    }

    async fn commit(
        &self,
        request: Request<CreditCommitRequest>,
    ) -> Result<Response<CreditCommitResponse>, Status> {
        let req = request.into_inner();
        let tenant = match self
            .reservation_contexts
            .read()
            .await
            .get(&req.reservation_id)
        {
            Some(ctx) => ctx.clone(),
            None => {
                return Ok(Response::new(CreditCommitResponse {
                    success: false,
                    reason: "reservation context not found".to_string(),
                }))
            }
        };

        let amount = i64::try_from(req.units)
            .map_err(|_| Status::invalid_argument("units exceeds i64 range"))?;
        let commit_request = CommitReservationRequest {
            reservation_id: req.reservation_id.clone(),
            actual_amount: amount,
            resource_id: String::new(),
        };

        let mut commit_request = Request::new(commit_request);
        commit_request.extensions_mut().insert(tenant);

        match self.credit_service.commit_reservation(commit_request).await {
            Ok(_) => {
                let mut guard = self.reservation_contexts.write().await;
                guard.remove(&req.reservation_id);
                Ok(Response::new(CreditCommitResponse {
                    success: true,
                    reason: String::new(),
                }))
            }
            Err(status) => match status.code() {
                Code::NotFound | Code::FailedPrecondition => {
                    Ok(Response::new(CreditCommitResponse {
                        success: false,
                        reason: status.message().to_string(),
                    }))
                }
                Code::InvalidArgument => Err(Status::invalid_argument(status.message())),
                _ => Err(status),
            },
        }
    }

    async fn rollback(
        &self,
        request: Request<CreditRollbackRequest>,
    ) -> Result<Response<CreditRollbackResponse>, Status> {
        let req = request.into_inner();
        let tenant = match self
            .reservation_contexts
            .read()
            .await
            .get(&req.reservation_id)
        {
            Some(ctx) => ctx.clone(),
            None => {
                return Ok(Response::new(CreditRollbackResponse {
                    success: false,
                    reason: "reservation context not found".to_string(),
                }))
            }
        };
        let rollback_request = ReleaseReservationRequest {
            reservation_id: req.reservation_id.clone(),
            reason: "gateway rollback".into(),
        };
        let mut rollback_request = Request::new(rollback_request);
        rollback_request.extensions_mut().insert(tenant);

        match self
            .credit_service
            .release_reservation(rollback_request)
            .await
        {
            Ok(response) => {
                let response = response.into_inner();
                if response.success {
                    let mut guard = self.reservation_contexts.write().await;
                    guard.remove(&req.reservation_id);
                }
                Ok(Response::new(CreditRollbackResponse {
                    success: response.success,
                    reason: String::new(),
                }))
            }
            Err(status) => match status.code() {
                Code::NotFound | Code::FailedPrecondition => {
                    Ok(Response::new(CreditRollbackResponse {
                        success: false,
                        reason: status.message().to_string(),
                    }))
                }
                Code::InvalidArgument => Err(Status::invalid_argument(status.message())),
                _ => Err(status),
            },
        }
    }
}

fn deny_response(reason: impl Into<String>) -> CreditReserveResponse {
    CreditReserveResponse {
        allow: false,
        reservation_id: String::new(),
        reason: reason.into(),
        remaining: 0,
    }
}

fn reservation_resource_type(req: &CreditReserveRequest) -> String {
    if let Some(value) = req.attributes.get("resource_type") {
        return value.clone();
    }
    if !req.route_name.is_empty() {
        return req.route_name.clone();
    }
    "apigateway".to_string()
}

fn reservation_description(req: &CreditReserveRequest) -> String {
    if let Some(value) = req.attributes.get("description") {
        return value.clone();
    }

    let mut parts = vec![format!("route={}", req.route_name)];
    if !req.method.is_empty() {
        parts.push(format!("method={}", req.method));
    }
    if !req.path.is_empty() {
        parts.push(format!("path={}", req.path));
    }
    if !req.request_id.is_empty() {
        parts.push(format!("request_id={}", req.request_id));
    }
    if !req.subject_id.is_empty() {
        parts.push(format!("subject_id={}", req.subject_id));
    }

    format!("gateway {}", parts.join(" "))
}

fn reservation_ttl(attributes: &HashMap<String, String>) -> i32 {
    attributes
        .get("ttl_seconds")
        .and_then(|value| value.parse::<i32>().ok())
        .unwrap_or(0)
}

fn tenant_context_from_reserve(req: &CreditReserveRequest) -> Result<TenantContext, Status> {
    if req.org_id.trim().is_empty() {
        return Err(Status::invalid_argument(
            "org_id required for credit reservation",
        ));
    }
    if req.project_id.trim().is_empty() {
        return Err(Status::invalid_argument(
            "project_id required for credit reservation",
        ));
    }

    let (principal_id, principal_kind) = if req.subject_id.is_empty() {
        ("apigateway".to_string(), PrincipalKind::ServiceAccount)
    } else {
        (req.subject_id.clone(), PrincipalKind::User)
    };

    Ok(TenantContext {
        org_id: req.org_id.clone(),
        project_id: req.project_id.clone(),
        principal_id: principal_id.clone(),
        principal_name: principal_id,
        principal_kind,
        node_id: None,
    })
}

/*
#[cfg(test)]
mod tests {
    use super::*;
    use crate::storage::{CreditStorage, InMemoryStorage};
    use creditservice_types::Wallet;

    fn reserve_request(project_id: &str, units: u64) -> CreditReserveRequest {
        CreditReserveRequest {
            request_id: "req-1".into(),
            subject_id: "subject-1".into(),
            org_id: "org-1".into(),
            project_id: project_id.into(),
            route_name: "route-1".into(),
            method: "GET".into(),
            path: "/v1/test".into(),
            raw_query: "".into(),
            units,
            attributes: HashMap::new(),
        }
    }

    #[tokio::test]
    async fn test_reserve_denied_without_wallet() {
        let storage = InMemoryStorage::new();
        let credit_service = Arc::new(CreditServiceImpl::new(storage));
        let gateway = GatewayCreditServiceImpl::new(credit_service);

        let response = gateway
            .reserve(Request::new(reserve_request("proj-1", 5)))
            .await
            .unwrap()
            .into_inner();

        assert!(!response.allow);
        assert!(response.reason.contains("Wallet not found"));
    }

    #[tokio::test]
    async fn test_reserve_commit_success() {
        let storage = InMemoryStorage::new();
        let wallet = Wallet::new("proj-1".into(), "org-1".into(), 100);
        storage.create_wallet(wallet).await.unwrap();

        let credit_service = Arc::new(CreditServiceImpl::new(storage));
        let gateway = GatewayCreditServiceImpl::new(credit_service);

        let reserve_response = gateway
            .reserve(Request::new(reserve_request("proj-1", 10)))
            .await
            .unwrap()
            .into_inner();

        assert!(reserve_response.allow);
        assert!(!reserve_response.reservation_id.is_empty());

        let commit_response = gateway
            .commit(Request::new(CreditCommitRequest {
                reservation_id: reserve_response.reservation_id,
                units: 10,
            }))
            .await
            .unwrap()
            .into_inner();

        assert!(commit_response.success);
    }
}
*/
