//! RecordService gRPC implementation

use std::sync::Arc;

use base64::Engine as _;
use crate::metadata::DnsMetadataStore;
use flashdns_api::proto::{
    record_data, ARecord as ProtoARecord, AaaaRecord as ProtoAaaaRecord,
    BatchCreateRecordsRequest, BatchCreateRecordsResponse, BatchDeleteRecordsRequest,
    CaaRecord as ProtoCaaRecord, CnameRecord as ProtoCnameRecord, CreateRecordRequest,
    CreateRecordResponse, DeleteRecordRequest, GetRecordRequest, GetRecordResponse,
    ListRecordsRequest, ListRecordsResponse, MxRecord as ProtoMxRecord, NsRecord as ProtoNsRecord,
    PtrRecord as ProtoPtrRecord, RecordData as ProtoRecordData, RecordInfo,
    SrvRecord as ProtoSrvRecord, TxtRecord as ProtoTxtRecord, UpdateRecordRequest,
    UpdateRecordResponse,
};
use flashdns_api::RecordService;
use flashdns_types::{Record, RecordData, RecordId, RecordType, Ttl, ZoneId};
use iam_service_auth::{get_tenant_context, resource_for_tenant, AuthService};
use prost_types::Timestamp;
use tonic::{Request, Response, Status};

/// RecordService implementation
pub struct RecordServiceImpl {
    metadata: Arc<DnsMetadataStore>,
    auth: Arc<AuthService>,
}

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

const ACTION_RECORDS_CREATE: &str = "dns:records:create";
const ACTION_RECORDS_READ: &str = "dns:records:read";
const ACTION_RECORDS_UPDATE: &str = "dns:records:update";
const ACTION_RECORDS_DELETE: &str = "dns:records:delete";
const ACTION_RECORDS_LIST: &str = "dns:records:list";

/// Convert Record to proto RecordInfo
fn record_to_proto(record: &Record) -> RecordInfo {
    RecordInfo {
        id: record.id.to_string(),
        zone_id: record.zone_id.to_string(),
        name: record.name.clone(),
        record_type: record.record_type.to_string(),
        ttl: record.ttl.as_secs(),
        data: Some(record_data_to_proto(&record.data)),
        enabled: record.enabled,
        created_at: Some(Timestamp {
            seconds: record.created_at.timestamp(),
            nanos: record.created_at.timestamp_subsec_nanos() as i32,
        }),
        updated_at: Some(Timestamp {
            seconds: record.updated_at.timestamp(),
            nanos: record.updated_at.timestamp_subsec_nanos() as i32,
        }),
    }
}

/// Convert RecordData to proto RecordData
fn record_data_to_proto(data: &RecordData) -> ProtoRecordData {
    let inner = match data {
        RecordData::A { address } => record_data::Data::A(ProtoARecord {
            address: format!("{}.{}.{}.{}", address[0], address[1], address[2], address[3]),
        }),
        RecordData::Aaaa { address } => {
            // Format IPv6 address
            let addr = std::net::Ipv6Addr::from(*address);
            record_data::Data::Aaaa(ProtoAaaaRecord {
                address: addr.to_string(),
            })
        }
        RecordData::Cname { target } => record_data::Data::Cname(ProtoCnameRecord {
            target: target.clone(),
        }),
        RecordData::Mx {
            preference,
            exchange,
        } => record_data::Data::Mx(ProtoMxRecord {
            preference: *preference as u32,
            exchange: exchange.clone(),
        }),
        RecordData::Txt { text } => record_data::Data::Txt(ProtoTxtRecord { text: text.clone() }),
        RecordData::Srv {
            priority,
            weight,
            port,
            target,
        } => record_data::Data::Srv(ProtoSrvRecord {
            priority: *priority as u32,
            weight: *weight as u32,
            port: *port as u32,
            target: target.clone(),
        }),
        RecordData::Ns { nameserver } => record_data::Data::Ns(ProtoNsRecord {
            nameserver: nameserver.clone(),
        }),
        RecordData::Ptr { target } => record_data::Data::Ptr(ProtoPtrRecord {
            target: target.clone(),
        }),
        RecordData::Caa { flags, tag, value } => record_data::Data::Caa(ProtoCaaRecord {
            flags: *flags as u32,
            tag: tag.clone(),
            value: value.clone(),
        }),
    };
    ProtoRecordData { data: Some(inner) }
}

/// Parse proto RecordData to RecordData
fn proto_to_record_data(proto: &ProtoRecordData) -> Result<RecordData, Status> {
    let data = proto
        .data
        .as_ref()
        .ok_or_else(|| Status::invalid_argument("record data is required"))?;

    match data {
        record_data::Data::A(a) => {
            let parts: Vec<&str> = a.address.split('.').collect();
            if parts.len() != 4 {
                return Err(Status::invalid_argument("invalid IPv4 address"));
            }
            let mut octets = [0u8; 4];
            for (i, part) in parts.iter().enumerate() {
                octets[i] = part
                    .parse()
                    .map_err(|_| Status::invalid_argument("invalid IPv4 octet"))?;
            }
            Ok(RecordData::A { address: octets })
        }
        record_data::Data::Aaaa(aaaa) => {
            let addr: std::net::Ipv6Addr = aaaa
                .address
                .parse()
                .map_err(|_| Status::invalid_argument("invalid IPv6 address"))?;
            Ok(RecordData::Aaaa {
                address: addr.octets(),
            })
        }
        record_data::Data::Cname(cname) => Ok(RecordData::Cname {
            target: cname.target.clone(),
        }),
        record_data::Data::Mx(mx) => Ok(RecordData::Mx {
            preference: mx.preference as u16,
            exchange: mx.exchange.clone(),
        }),
        record_data::Data::Txt(txt) => Ok(RecordData::Txt {
            text: txt.text.clone(),
        }),
        record_data::Data::Srv(srv) => Ok(RecordData::Srv {
            priority: srv.priority as u16,
            weight: srv.weight as u16,
            port: srv.port as u16,
            target: srv.target.clone(),
        }),
        record_data::Data::Ns(ns) => Ok(RecordData::Ns {
            nameserver: ns.nameserver.clone(),
        }),
        record_data::Data::Ptr(ptr) => Ok(RecordData::Ptr {
            target: ptr.target.clone(),
        }),
        record_data::Data::Caa(caa) => Ok(RecordData::Caa {
            flags: caa.flags as u8,
            tag: caa.tag.clone(),
            value: caa.value.clone(),
        }),
    }
}

/// Parse record type from string
fn parse_record_type(s: &str) -> Result<RecordType, Status> {
    match s.to_uppercase().as_str() {
        "A" => Ok(RecordType::A),
        "AAAA" => Ok(RecordType::Aaaa),
        "CNAME" => Ok(RecordType::Cname),
        "MX" => Ok(RecordType::Mx),
        "TXT" => Ok(RecordType::Txt),
        "SRV" => Ok(RecordType::Srv),
        "NS" => Ok(RecordType::Ns),
        "PTR" => Ok(RecordType::Ptr),
        "CAA" => Ok(RecordType::Caa),
        "SOA" => Ok(RecordType::Soa),
        _ => Err(Status::invalid_argument(format!(
            "unsupported record type: {}",
            s
        ))),
    }
}

#[tonic::async_trait]
impl RecordService for RecordServiceImpl {
    async fn create_record(
        &self,
        request: Request<CreateRecordRequest>,
    ) -> Result<Response<CreateRecordResponse>, Status> {
        let tenant = get_tenant_context(&request)?;
        let req = request.into_inner();

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

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

        // Verify zone exists
        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_RECORDS_CREATE,
                &resource_for_tenant("record", "*", &zone.org_id, &zone.project_id),
            )
            .await?;

        // Parse record data
        let record_data = proto_to_record_data(
            req.data
                .as_ref()
                .ok_or_else(|| Status::invalid_argument("record data is required"))?,
        )?;

        // Create record
        let mut record = Record::new(zone_id, &req.name, record_data);

        // Apply TTL if provided
        if req.ttl > 0 {
            record.ttl = Ttl::new(req.ttl)
                .map_err(|e| Status::invalid_argument(format!("invalid TTL: {}", e)))?;
        }

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

        Ok(Response::new(CreateRecordResponse {
            record: Some(record_to_proto(&record)),
        }))
    }

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

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

        let record_id: RecordId = req
            .id
            .parse()
            .map_err(|_| Status::invalid_argument("invalid record ID"))?;

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

        let zone = self
            .metadata
            .load_zone_by_id(&record.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_RECORDS_READ,
                &resource_for_tenant("record", &record.id.to_string(), &zone.org_id, &zone.project_id),
            )
            .await?;

        Ok(Response::new(GetRecordResponse {
            record: Some(record_to_proto(&record)),
        }))
    }

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

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

        let zone_id: ZoneId = req
            .zone_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_RECORDS_LIST,
                &resource_for_tenant("record", "*", &zone.org_id, &zone.project_id),
            )
            .await?;

        let records = self
            .metadata
            .list_records(&zone_id)
            .await
            .map_err(|e| Status::internal(format!("metadata error: {}", e)))?;

        // Apply filters
        let filtered: Vec<_> = records
            .into_iter()
            .filter(|r| {
                // Name filter
                if !req.name_filter.is_empty() && !r.name.contains(&req.name_filter) {
                    return false;
                }
                // Type filter
                if !req.type_filter.is_empty() {
                    if let Ok(filter_type) = parse_record_type(&req.type_filter) {
                        if r.record_type != filter_type {
                            return false;
                        }
                    }
                }
                true
            })
            .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_records: 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 record_infos: Vec<RecordInfo> = paginated_records.iter().map(|r| record_to_proto(r)).collect();

        Ok(Response::new(ListRecordsResponse {
            records: record_infos,
            next_page_token,
        }))
    }

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

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

        let record_id: RecordId = req
            .id
            .parse()
            .map_err(|_| Status::invalid_argument("invalid record ID"))?;

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

        let zone = self
            .metadata
            .load_zone_by_id(&record.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_RECORDS_UPDATE,
                &resource_for_tenant("record", &record.id.to_string(), &zone.org_id, &zone.project_id),
            )
            .await?;

        // Apply updates
        if let Some(ttl) = req.ttl {
            record.ttl = Ttl::new(ttl)
                .map_err(|e| Status::invalid_argument(format!("invalid TTL: {}", e)))?;
        }
        if let Some(ref data) = req.data {
            record.data = proto_to_record_data(data)?;
            record.record_type = record.data.record_type();
        }
        if let Some(enabled) = req.enabled {
            record.enabled = enabled;
        }

        // Update timestamp
        record.updated_at = chrono::Utc::now();

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

        Ok(Response::new(UpdateRecordResponse {
            record: Some(record_to_proto(&record)),
        }))
    }

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

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

        let record_id: RecordId = req
            .id
            .parse()
            .map_err(|_| Status::invalid_argument("invalid record ID"))?;

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

        let zone = self
            .metadata
            .load_zone_by_id(&record.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_RECORDS_DELETE,
                &resource_for_tenant("record", &record.id.to_string(), &zone.org_id, &zone.project_id),
            )
            .await?;

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

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

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

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

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

        // Verify zone exists
        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_RECORDS_CREATE,
                &resource_for_tenant("record", "*", &zone.org_id, &zone.project_id),
            )
            .await?;

        let mut created_records = Vec::new();

        for record_req in req.records {
            // Parse record data
            let record_data = proto_to_record_data(
                record_req
                    .data
                    .as_ref()
                    .ok_or_else(|| Status::invalid_argument("record data is required"))?,
            )?;

            // Create record
            let mut record = Record::new(zone_id, &record_req.name, record_data);

            // Apply TTL if provided
            if record_req.ttl > 0 {
                record.ttl = Ttl::new(record_req.ttl)
                    .map_err(|e| Status::invalid_argument(format!("invalid TTL: {}", e)))?;
            }

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

            created_records.push(record_to_proto(&record));
        }

        Ok(Response::new(BatchCreateRecordsResponse {
            records: created_records,
        }))
    }

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

        for id in req.ids {
            let record_id: RecordId = id
                .parse()
                .map_err(|_| Status::invalid_argument("invalid record ID"))?;

            if let Some(record) = self
                .metadata
                .load_record_by_id(&record_id)
                .await
                .map_err(|e| Status::internal(format!("metadata error: {}", e)))?
            {
                let zone = self
                    .metadata
                    .load_zone_by_id(&record.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_RECORDS_DELETE,
                        &resource_for_tenant(
                            "record",
                            &record.id.to_string(),
                            &zone.org_id,
                            &zone.project_id,
                        ),
                    )
                    .await?;

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

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