use crate::Alignment;
use crate::FwfError;
use crate::TruncationPolicy;
use crate::fw_error::Result;
use crate::fw_writer_field::FwWriterField;
use crate::record_separator::RecordSeparator;

/// A reusable record-shape definition for an [`FwWriter`](crate::FwWriter).
///
/// A schema describes the fields written into each record, the optional
/// record separator, and the global truncation policy applied when a value
/// does not fit its field. It also owns the internal buffer used to format
/// records, so swapping schemas via
/// [`FwWriter::into_writer_for_schema`](crate::FwWriter::into_writer_for_schema)
/// is allocation-free for both the outgoing and incoming schema.
///
/// Construct one via [`FwWriterSchema::builder`].
#[derive(Debug)]
pub struct FwWriterSchema {
    fields: Vec<FwWriterField>,
    separator: RecordSeparator,
    truncation: TruncationPolicy,
    record_buffer: Vec<u8>,
}

impl FwWriterSchema {
    pub(crate) fn new(
        fields: Vec<FwWriterField>,
        separator: RecordSeparator,
        truncation: TruncationPolicy,
    ) -> Self {
        Self {
            fields,
            separator,
            truncation,
            record_buffer: Vec::new(),
        }
    }

    /// Returns a [`FwWriterSchemaBuilder`] for configuring a new schema.
    #[must_use]
    #[inline]
    pub fn builder() -> FwWriterSchemaBuilder {
        FwWriterSchemaBuilder::new()
    }

    #[must_use]
    pub(crate) fn record_buffer(&self) -> &[u8] {
        &self.record_buffer
    }

    /// Formats a byte record into the internal buffer.
    pub(crate) fn prepare_byte_record(&mut self, values: &[&[u8]]) -> Result<()> {
        let expected = self.fields.len();
        if values.len() != expected {
            return Err(FwfError::ValueCountMismatch {
                provided: values.len(),
                expected,
            });
        }

        self.record_buffer.clear();
        let global_truncation = self.truncation;
        for (field_index, field) in self.fields.iter().enumerate() {
            let value = values[field_index];
            let truncation = field.truncation().unwrap_or(global_truncation);
            let value = truncate_bytes(value, field.width(), truncation, field_index)?;
            write_padded(
                &mut self.record_buffer,
                value,
                field.width(),
                field.padding(),
                field.alignment(),
            );
        }
        self.record_buffer
            .extend_from_slice(self.separator.as_bytes());
        Ok(())
    }

    /// Formats a string record into the internal buffer.
    #[cfg(feature = "encoding_rs")]
    pub(crate) fn prepare_string_record(
        &mut self,
        values: &[&str],
        encoding: &'static encoding_rs::Encoding,
    ) -> Result<()> {
        let expected = self.fields.len();
        if values.len() != expected {
            return Err(FwfError::ValueCountMismatch {
                provided: values.len(),
                expected,
            });
        }

        self.record_buffer.clear();
        let global_truncation = self.truncation;
        for (field_index, field) in self.fields.iter().enumerate() {
            let value = values[field_index];
            let truncation = field.truncation().unwrap_or(global_truncation);
            let encoded =
                encode_with_truncation(value, encoding, field.width(), truncation, field_index)?;
            write_padded(
                &mut self.record_buffer,
                &encoded,
                field.width(),
                field.padding(),
                field.alignment(),
            );
        }
        self.record_buffer
            .extend_from_slice(self.separator.as_bytes());
        Ok(())
    }
}

/// Builder for constructing a [`FwWriterSchema`].
///
/// Created via [`FwWriterSchema::builder`]. Add fields with
/// [`field`](Self::field), set the record separator with
/// [`separator`](Self::separator), set the global truncation policy with
/// [`truncation`](Self::truncation), and call [`build`](Self::build) to
/// produce a schema.
#[derive(Debug, Clone)]
pub struct FwWriterSchemaBuilder {
    fields: Vec<FwWriterField>,
    separator: RecordSeparator,
    truncation: TruncationPolicy,
}

impl FwWriterSchemaBuilder {
    #[must_use]
    pub(crate) fn new() -> Self {
        Self {
            fields: Vec::new(),
            separator: RecordSeparator::default(),
            truncation: TruncationPolicy::default(),
        }
    }

    /// Adds a field to the schema. Fields are written in the order they are added.
    #[must_use]
    #[inline]
    pub fn field(mut self, field: FwWriterField) -> Self {
        self.fields.push(field);
        self
    }

    /// Sets the record separator. Defaults to [`RecordSeparator::None`].
    #[must_use]
    #[inline]
    pub fn separator(mut self, separator: RecordSeparator) -> Self {
        self.separator = separator;
        self
    }

    /// Sets the global truncation policy applied when a value does not fit
    /// its field, unless overridden per-field. Defaults to
    /// [`crate::TruncationPolicy::Error`].
    #[must_use]
    #[inline]
    pub fn truncation(mut self, truncation: TruncationPolicy) -> Self {
        self.truncation = truncation;
        self
    }

    /// Produces a [`FwWriterSchema`] with the configured fields, separator,
    /// and truncation policy.
    #[must_use]
    #[inline]
    pub fn build(self) -> FwWriterSchema {
        FwWriterSchema::new(self.fields, self.separator, self.truncation)
    }
}

/// Truncates a byte slice to fit within `width`, respecting the truncation policy.
fn truncate_bytes(
    value: &'_ [u8],
    width: usize,
    truncation: TruncationPolicy,
    field_index: usize,
) -> Result<&'_ [u8]> {
    if value.len() <= width {
        return Ok(value);
    }
    match truncation {
        TruncationPolicy::Error => Err(FwfError::ValueTooLong {
            field_index,
            value_len: value.len(),
        }),
        TruncationPolicy::Left => Ok(&value[value.len() - width..]),
        TruncationPolicy::Right => Ok(&value[..width]),
    }
}

/// Writes a value with padding and alignment into a byte buffer.
fn write_padded(
    buffer: &mut Vec<u8>,
    value: &[u8],
    width: usize,
    padding: u8,
    alignment: Alignment,
) {
    let pad_count = width - value.len();
    match alignment {
        Alignment::Left => {
            buffer.extend_from_slice(value);
            buffer.resize(buffer.len() + pad_count, padding);
        }
        Alignment::Right => {
            buffer.resize(buffer.len() + pad_count, padding);
            buffer.extend_from_slice(value);
        }
    }
}

/// Encodes a string, truncating at character boundaries if the result exceeds `width`.
#[cfg(feature = "encoding_rs")]
fn encode_with_truncation<'a>(
    value: &'a str,
    encoding: &'static encoding_rs::Encoding,
    width: usize,
    truncation: TruncationPolicy,
    field_index: usize,
) -> Result<std::borrow::Cow<'a, [u8]>> {
    let (encoded, _, had_errors) = encoding.encode(value);
    if had_errors {
        return Err(FwfError::Encoding { field_index });
    }
    if encoded.len() <= width {
        return Ok(encoded);
    }
    match truncation {
        TruncationPolicy::Error => Err(FwfError::ValueTooLong {
            field_index,
            value_len: encoded.len(),
        }),
        TruncationPolicy::Right => {
            let mut best = 0;
            for (i, c) in value.char_indices() {
                let end = i + c.len_utf8();
                let (enc, _, _) = encoding.encode(&value[..end]);
                if enc.len() <= width {
                    best = end;
                } else {
                    break;
                }
            }
            let (result, _, _) = encoding.encode(&value[..best]);
            Ok(result)
        }
        TruncationPolicy::Left => {
            let mut best = value.len();
            for (i, _) in value.char_indices().rev() {
                let (enc, _, _) = encoding.encode(&value[i..]);
                if enc.len() <= width {
                    best = i;
                } else {
                    break;
                }
            }
            let (result, _, _) = encoding.encode(&value[best..]);
            Ok(result)
        }
    }
}
