//! ZoneService gRPC implementation

use std::sync::Arc;

use base64::Engine as _;
use crate::metadata::DnsMetadataStore;
use flashdns_api::proto::{
    CreateZoneRequest, CreateZoneResponse, DeleteZoneRequest, DisableZoneRequest,
    EnableZoneRequest, GetZoneRequest, GetZoneResponse, ListZonesRequest, ListZonesResponse,
    UpdateZoneRequest, UpdateZoneResponse, ZoneInfo,
};
use flashdns_api::ZoneService;
use flashdns_types::{Zone, ZoneId, ZoneName, ZoneStatus};
use iam_service_auth::{get_tenant_context, resolve_tenant_ids_from_context, resource_for_tenant, AuthService};
use prost_types::Timestamp;
use tonic::{Request, Response, Status};

/// ZoneService implementation
pub struct ZoneServiceImpl {
    metadata: Arc<DnsMetadataStore>,
    auth: Arc<AuthService>,
}

impl ZoneServiceImpl {
    /// Create a new ZoneService with metadata store
    pub fn new(metadata: Arc<DnsMetadataStore>, auth: Arc<AuthService>) -> Self {
        Self { metadata, auth }
    }
}

const ACTION_ZONES_CREATE: &str = "dns:zones:create";
const ACTION_ZONES_READ: &str = "dns:zones:read";
const ACTION_ZONES_UPDATE: &str = "dns:zones:update";
const ACTION_ZONES_DELETE: &str = "dns:zones:delete";
const ACTION_ZONES_LIST: &str = "dns:zones:list";

/// Convert Zone to proto ZoneInfo
fn zone_to_proto(zone: &Zone) -> ZoneInfo {
    ZoneInfo {
        id: zone.id.to_string(),
        name: zone.name.as_str().to_string(),
        org_id: zone.org_id.clone(),
        project_id: zone.project_id.clone(),
        status: match zone.status {
            ZoneStatus::Active => "active".to_string(),
            ZoneStatus::Creating => "creating".to_string(),
            ZoneStatus::Disabled => "disabled".to_string(),
            ZoneStatus::Deleting => "deleting".to_string(),
        },
        serial: zone.serial,
        refresh: zone.refresh,
        retry: zone.retry,
        expire: zone.expire,
        minimum: zone.minimum,
        primary_ns: zone.primary_ns.clone(),
        admin_email: zone.admin_email.clone(),
        created_at: Some(Timestamp {
            seconds: zone.created_at.timestamp(),
            nanos: zone.created_at.timestamp_subsec_nanos() as i32,
        }),
        updated_at: Some(Timestamp {
            seconds: zone.updated_at.timestamp(),
            nanos: zone.updated_at.timestamp_subsec_nanos() as i32,
        }),
        record_count: zone.record_count,
    }
}

/// Parse ZoneStatus from string
fn parse_zone_status(s: &str) -> ZoneStatus {
    match s.to_lowercase().as_str() {
        "active" => ZoneStatus::Active,
        "creating" => ZoneStatus::Creating,
        "disabled" => ZoneStatus::Disabled,
        "deleting" => ZoneStatus::Deleting,
        _ => ZoneStatus::Active,
    }
}

#[tonic::async_trait]
impl ZoneService for ZoneServiceImpl {
    async fn create_zone(
        &self,
        request: Request<CreateZoneRequest>,
    ) -> Result<Response<CreateZoneResponse>, Status> {
        let tenant = get_tenant_context(&request)?;
        let req = request.into_inner();
        let (org_id, project_id) =
            resolve_tenant_ids_from_context(&tenant, &req.org_id, &req.project_id)?;

        self.auth
            .authorize(
                &tenant,
                ACTION_ZONES_CREATE,
                &resource_for_tenant("zone", "*", &org_id, &project_id),
            )
            .await?;

        // Validate required fields
        if req.name.is_empty() {
            return Err(Status::invalid_argument("zone name is required"));
        }

        // Parse zone name
        let zone_name = ZoneName::new(&req.name)
            .map_err(|e| Status::invalid_argument(format!("invalid zone name: {}", e)))?;

        // Check if zone already exists
        if let Some(_existing) = self
            .metadata
            .load_zone(&org_id, &project_id, zone_name.as_str())
            .await
            .map_err(|e| Status::internal(format!("metadata error: {}", e)))?
        {
            return Err(Status::already_exists("zone already exists"));
        }

        // Create new zone
        let mut zone = Zone::new(zone_name, &org_id, &project_id);

        // Apply optional SOA parameters
        if !req.primary_ns.is_empty() {
            zone.primary_ns = req.primary_ns;
        }
        if !req.admin_email.is_empty() {
            zone.admin_email = req.admin_email;
        }

        // Save zone
        self.metadata
            .save_zone(&zone)
            .await
            .map_err(|e| Status::internal(format!("failed to save zone: {}", e)))?;

        Ok(Response::new(CreateZoneResponse {
            zone: Some(zone_to_proto(&zone)),
        }))
    }

    async fn get_zone(
        &self,
        request: Request<GetZoneRequest>,
    ) -> Result<Response<GetZoneResponse>, Status> {
        let tenant = get_tenant_context(&request)?;
        let req = request.into_inner();

        let zone = match req.identifier {
            Some(flashdns_api::proto::get_zone_request::Identifier::Id(id)) => {
                let zone_id: ZoneId = id
                    .parse()
                    .map_err(|_| Status::invalid_argument("invalid zone ID"))?;
                self.metadata
                    .load_zone_by_id(&zone_id)
                    .await
                    .map_err(|e| Status::internal(format!("metadata error: {}", e)))?
            }
            Some(flashdns_api::proto::get_zone_request::Identifier::Name(name)) => {
                let zone_name = ZoneName::new(&name)
                    .map_err(|e| Status::invalid_argument(format!("invalid zone name: {}", e)))?;
                self.metadata
                    .load_zone(&tenant.org_id, &tenant.project_id, zone_name.as_str())
                    .await
                    .map_err(|e| Status::internal(format!("metadata error: {}", e)))?
            }
            None => {
                return Err(Status::invalid_argument("zone identifier is required"));
            }
        };

        match zone {
            Some(z) => {
                if z.org_id != tenant.org_id || z.project_id != tenant.project_id {
                    return Err(Status::permission_denied("zone not in tenant scope"));
                }

                self.auth
                    .authorize(
                        &tenant,
                        ACTION_ZONES_READ,
                        &resource_for_tenant("zone", &z.id.to_string(), &z.org_id, &z.project_id),
                    )
                    .await?;

                Ok(Response::new(GetZoneResponse {
                    zone: Some(zone_to_proto(&z)),
                }))
            }
            None => Err(Status::not_found("zone not found")),
        }
    }

    async fn list_zones(
        &self,
        request: Request<ListZonesRequest>,
    ) -> Result<Response<ListZonesResponse>, Status> {
        let tenant = get_tenant_context(&request)?;
        let req = request.into_inner();
        let (org_id, project_id) =
            resolve_tenant_ids_from_context(&tenant, &req.org_id, &req.project_id)?;

        self.auth
            .authorize(
                &tenant,
                ACTION_ZONES_LIST,
                &resource_for_tenant("zone", "*", &org_id, &project_id),
            )
            .await?;

        let zones = self
            .metadata
            .list_zones(&org_id, Some(project_id.as_str()))
            .await
            .map_err(|e| Status::internal(format!("metadata error: {}", e)))?;

        // Apply name filter if provided
        let filtered: Vec<_> = if req.name_filter.is_empty() {
            zones
        } else {
            zones
                .into_iter()
                .filter(|z| z.name.as_str().contains(&req.name_filter))
                .collect()
        };

        // Implement pagination using page_size and page_token
        let page_size = if req.page_size == 0 {
            50 // Default page size
        } else {
            req.page_size as usize
        };

        // Parse page_token as base64-encoded offset
        let offset = if req.page_token.is_empty() {
            0
        } else {
            let decoded = base64::Engine::decode(
                &base64::engine::general_purpose::STANDARD,
                &req.page_token,
            )
            .map_err(|_| Status::invalid_argument("invalid page_token"))?;

            let offset_str = String::from_utf8(decoded)
                .map_err(|_| Status::invalid_argument("invalid page_token encoding"))?;

            offset_str
                .parse::<usize>()
                .map_err(|_| Status::invalid_argument("invalid page_token format"))?
        };

        // Apply pagination
        let total = filtered.len();
        let end = std::cmp::min(offset + page_size, total);
        let paginated_zones: Vec<_> = filtered.iter().skip(offset).take(page_size).collect();

        // Generate next_page_token if there are more results
        let next_page_token = if end < total {
            let next_offset = end.to_string();
            base64::Engine::encode(
                &base64::engine::general_purpose::STANDARD,
                next_offset.as_bytes(),
            )
        } else {
            String::new()
        };

        let zone_infos: Vec<ZoneInfo> = paginated_zones.iter().map(|z| zone_to_proto(z)).collect();

        Ok(Response::new(ListZonesResponse {
            zones: zone_infos,
            next_page_token,
        }))
    }

    async fn update_zone(
        &self,
        request: Request<UpdateZoneRequest>,
    ) -> Result<Response<UpdateZoneResponse>, Status> {
        let tenant = get_tenant_context(&request)?;
        let req = request.into_inner();

        if req.id.is_empty() {
            return Err(Status::invalid_argument("zone id is required"));
        }

        let zone_id: ZoneId = req
            .id
            .parse()
            .map_err(|_| Status::invalid_argument("invalid zone ID"))?;

        let mut zone = self
            .metadata
            .load_zone_by_id(&zone_id)
            .await
            .map_err(|e| Status::internal(format!("metadata error: {}", e)))?
            .ok_or_else(|| Status::not_found("zone not found"))?;

        if zone.org_id != tenant.org_id || zone.project_id != tenant.project_id {
            return Err(Status::permission_denied("zone not in tenant scope"));
        }

        self.auth
            .authorize(
                &tenant,
                ACTION_ZONES_UPDATE,
                &resource_for_tenant("zone", &zone.id.to_string(), &zone.org_id, &zone.project_id),
            )
            .await?;

        // Apply updates
        if let Some(refresh) = req.refresh {
            zone.refresh = refresh;
        }
        if let Some(retry) = req.retry {
            zone.retry = retry;
        }
        if let Some(expire) = req.expire {
            zone.expire = expire;
        }
        if let Some(minimum) = req.minimum {
            zone.minimum = minimum;
        }
        if let Some(ref primary_ns) = req.primary_ns {
            zone.primary_ns = primary_ns.clone();
        }
        if let Some(ref admin_email) = req.admin_email {
            zone.admin_email = admin_email.clone();
        }

        // Increment serial
        zone.increment_serial();

        // Save updated zone
        self.metadata
            .save_zone(&zone)
            .await
            .map_err(|e| Status::internal(format!("failed to save zone: {}", e)))?;

        Ok(Response::new(UpdateZoneResponse {
            zone: Some(zone_to_proto(&zone)),
        }))
    }

    async fn delete_zone(
        &self,
        request: Request<DeleteZoneRequest>,
    ) -> Result<Response<()>, Status> {
        let tenant = get_tenant_context(&request)?;
        let req = request.into_inner();

        if req.id.is_empty() {
            return Err(Status::invalid_argument("zone id is required"));
        }

        let zone_id: ZoneId = req
            .id
            .parse()
            .map_err(|_| Status::invalid_argument("invalid zone ID"))?;

        let zone = self
            .metadata
            .load_zone_by_id(&zone_id)
            .await
            .map_err(|e| Status::internal(format!("metadata error: {}", e)))?
            .ok_or_else(|| Status::not_found("zone not found"))?;

        if zone.org_id != tenant.org_id || zone.project_id != tenant.project_id {
            return Err(Status::permission_denied("zone not in tenant scope"));
        }

        self.auth
            .authorize(
                &tenant,
                ACTION_ZONES_DELETE,
                &resource_for_tenant("zone", &zone.id.to_string(), &zone.org_id, &zone.project_id),
            )
            .await?;

        // Check for records if not force delete
        if !req.force {
            let records = self
                .metadata
                .list_records(&zone_id)
                .await
                .map_err(|e| Status::internal(format!("metadata error: {}", e)))?;

            if !records.is_empty() {
                return Err(Status::failed_precondition(
                    "zone has records; use force=true to delete anyway",
                ));
            }
        }

        // Delete all records first
        self.metadata
            .delete_zone_records(&zone_id)
            .await
            .map_err(|e| Status::internal(format!("failed to delete records: {}", e)))?;

        // Delete zone
        self.metadata
            .delete_zone(&zone)
            .await
            .map_err(|e| Status::internal(format!("failed to delete zone: {}", e)))?;

        Ok(Response::new(()))
    }

    async fn enable_zone(
        &self,
        request: Request<EnableZoneRequest>,
    ) -> Result<Response<()>, Status> {
        let tenant = get_tenant_context(&request)?;
        let req = request.into_inner();

        if req.id.is_empty() {
            return Err(Status::invalid_argument("zone id is required"));
        }

        let zone_id: ZoneId = req
            .id
            .parse()
            .map_err(|_| Status::invalid_argument("invalid zone ID"))?;

        let mut zone = self
            .metadata
            .load_zone_by_id(&zone_id)
            .await
            .map_err(|e| Status::internal(format!("metadata error: {}", e)))?
            .ok_or_else(|| Status::not_found("zone not found"))?;

        if zone.org_id != tenant.org_id || zone.project_id != tenant.project_id {
            return Err(Status::permission_denied("zone not in tenant scope"));
        }

        self.auth
            .authorize(
                &tenant,
                ACTION_ZONES_UPDATE,
                &resource_for_tenant("zone", &zone.id.to_string(), &zone.org_id, &zone.project_id),
            )
            .await?;

        zone.status = ZoneStatus::Active;
        zone.increment_serial();

        self.metadata
            .save_zone(&zone)
            .await
            .map_err(|e| Status::internal(format!("failed to save zone: {}", e)))?;

        Ok(Response::new(()))
    }

    async fn disable_zone(
        &self,
        request: Request<DisableZoneRequest>,
    ) -> Result<Response<()>, Status> {
        let tenant = get_tenant_context(&request)?;
        let req = request.into_inner();

        if req.id.is_empty() {
            return Err(Status::invalid_argument("zone id is required"));
        }

        let zone_id: ZoneId = req
            .id
            .parse()
            .map_err(|_| Status::invalid_argument("invalid zone ID"))?;

        let mut zone = self
            .metadata
            .load_zone_by_id(&zone_id)
            .await
            .map_err(|e| Status::internal(format!("metadata error: {}", e)))?
            .ok_or_else(|| Status::not_found("zone not found"))?;

        if zone.org_id != tenant.org_id || zone.project_id != tenant.project_id {
            return Err(Status::permission_denied("zone not in tenant scope"));
        }

        self.auth
            .authorize(
                &tenant,
                ACTION_ZONES_UPDATE,
                &resource_for_tenant("zone", &zone.id.to_string(), &zone.org_id, &zone.project_id),
            )
            .await?;

        zone.status = ZoneStatus::Disabled;
        zone.increment_serial();

        self.metadata
            .save_zone(&zone)
            .await
            .map_err(|e| Status::internal(format!("failed to save zone: {}", e)))?;

        Ok(Response::new(()))
    }
}
