use crate::budget::EnforcingPolicy;
use crate::live_events::LiveEvents;

use super::{Cfg, Error, Events, Options, YamlDeserializer};

fn normalize_str_input(input: &str) -> &str {
    // Normalize: ignore a single leading UTF-8 BOM if present.
    input.strip_prefix('\u{FEFF}').unwrap_or(input)
}

fn deserialize_with_scope<'de, R, F, W>(
    src: &mut LiveEvents<'de>,
    cfg: Cfg,
    f: F,
    wrap_err: W,
) -> Result<R, Error>
where
    for<'e> F: FnOnce(crate::Deserializer<'de, 'e>) -> Result<R, Error>,
    W: Fn(Error, &LiveEvents<'de>) -> Error,
{
    let value_res = crate::anchor_store::with_document_scope(|| {
        crate::properties_redaction::with_interp_redaction_scope(|| {
            super::with_root_redaction(YamlDeserializer::new(src, cfg), f)
        })
    });
    match value_res {
        Ok(v) => Ok(v),
        Err(e) => {
            if src.synthesized_null_emitted() {
                // If the only thing in the input was an empty document (synthetic null),
                // surface this as an EOF error to preserve expected error semantics
                // for incompatible target types (e.g., bool).
                Err(wrap_err(
                    Error::eof().with_location(src.last_location()),
                    src,
                ))
            } else {
                Err(wrap_err(e, src))
            }
        }
    }
}

fn enforce_single_document_and_finish<'de, W>(
    src: &mut LiveEvents<'de>,
    multiple_docs_hint: &'static str,
    wrap_err: W,
) -> Result<(), Error>
where
    W: Fn(Error, &LiveEvents<'de>) -> Error,
{
    // After finishing first document, peek ahead to detect either another document/content
    // or trailing garbage. If a scan error occurs but we have seen a DocumentEnd ("..."),
    // ignore the trailing garbage. Otherwise, surface the error.
    match src.peek() {
        Ok(Some(_)) => {
            return Err(wrap_err(
                Error::multiple_documents(multiple_docs_hint).with_location(src.last_location()),
                src,
            ));
        }
        Ok(None) => {}
        Err(e) => {
            if src.seen_doc_end() {
                // Trailing garbage after a proper document end marker is ignored.
            } else {
                return Err(wrap_err(e, src));
            }
        }
    }

    src.finish().map_err(|e| wrap_err(e, src))
}

/// Create a streaming [`crate::Deserializer`] for a YAML string and run a closure against it.
///
/// This is useful for tooling that needs access to the underlying Serde deserializer,
/// such as wrappers that report unknown/ignored fields (e.g. the `serde_ignored` crate)
/// or wrappers that augment error paths.
///
/// The deserializer borrows internal parsing state, so it cannot be returned directly.
/// Instead, you provide a closure `f` that performs the desired deserialization.
#[allow(deprecated)]
pub fn with_deserializer_from_str_with_options<'de, R, F>(
    input: &'de str,
    options: Options,
    f: F,
) -> Result<R, Error>
where
    for<'e> F: FnOnce(crate::Deserializer<'de, 'e>) -> Result<R, Error>,
{
    let input = normalize_str_input(input);

    let with_snippet = options.with_snippet;
    let crop_radius = options.crop_radius;

    let cfg = Cfg::from_options(&options);
    // Do not stop at DocumentEnd; we'll probe for trailing content/errors explicitly.
    let mut src = LiveEvents::from_str(input, options, false);

    let wrap_err = |e, src: &LiveEvents<'de>| {
        crate::maybe_with_snippet_from_events(e, input, src, with_snippet, crop_radius)
    };

    let value = deserialize_with_scope(&mut src, cfg, f, wrap_err)?;
    enforce_single_document_and_finish(
        &mut src,
        "use from_multiple or from_multiple_with_options",
        wrap_err,
    )?;
    Ok(value)
}

/// Convenience wrapper around [`with_deserializer_from_str_with_options`] using
/// [`Options::default`].
pub fn with_deserializer_from_str<'de, R, F>(input: &'de str, f: F) -> Result<R, Error>
where
    for<'e> F: FnOnce(crate::Deserializer<'de, 'e>) -> Result<R, Error>,
{
    with_deserializer_from_str_with_options(input, Options::default(), f)
}

/// Create a streaming [`crate::Deserializer`] for a UTF-8 byte slice and run a closure against it.
///
/// This is equivalent to [`with_deserializer_from_str`], but validates the input is UTF-8.
pub fn with_deserializer_from_slice<'de, R, F>(bytes: &'de [u8], f: F) -> Result<R, Error>
where
    for<'e> F: FnOnce(crate::Deserializer<'de, 'e>) -> Result<R, Error>,
{
    with_deserializer_from_slice_with_options(bytes, Options::default(), f)
}

/// Create a streaming [`crate::Deserializer`] for a UTF-8 byte slice with configurable [`Options`]
/// and run a closure against it.
pub fn with_deserializer_from_slice_with_options<'de, R, F>(
    bytes: &'de [u8],
    options: Options,
    f: F,
) -> Result<R, Error>
where
    for<'e> F: FnOnce(crate::Deserializer<'de, 'e>) -> Result<R, Error>,
{
    let s = std::str::from_utf8(bytes).map_err(|_| Error::InvalidUtf8Input)?;
    with_deserializer_from_str_with_options(s, options, f)
}

/// Create a streaming [`crate::Deserializer`] for any [`std::io::Read`] and run a closure against it.
///
/// This is the reader-based counterpart to [`with_deserializer_from_str`]. It consumes a
/// byte-oriented reader and streams events into the deserializer. The reader accepts BOM-marked
/// UTF-8, UTF-16LE, and UTF-16BE. If no recognized BOM is present, the input bytes are
/// treated as UTF-8.
pub fn with_deserializer_from_reader<R, Out, F>(reader: R, f: F) -> Result<Out, Error>
where
    for<'de, 'e> F: FnOnce(crate::Deserializer<'de, 'e>) -> Result<Out, Error>,
    R: std::io::Read,
{
    with_deserializer_from_reader_with_options(reader, Options::default(), f)
}

/// Create a streaming [`crate::Deserializer`] for any [`std::io::Read`] with configurable [`Options`]
/// and run a closure against it.
#[allow(deprecated)]
pub fn with_deserializer_from_reader_with_options<R, Out, F>(
    reader: R,
    options: Options,
    f: F,
) -> Result<Out, Error>
where
    for<'de, 'e> F: FnOnce(crate::Deserializer<'de, 'e>) -> Result<Out, Error>,
    R: std::io::Read,
{
    let with_snippet = options.with_snippet;
    let crop_radius = options.crop_radius;
    let cfg = Cfg::from_options(&options);
    let shared_ring = crate::ring_reader::SharedRingReader::new(reader);
    let ring_handle = crate::ring_reader::SharedRingReaderHandle::new(&shared_ring);
    let mut src = LiveEvents::from_reader(ring_handle, options, false, EnforcingPolicy::AllContent);

    let wrap_err = |e, src: &LiveEvents<'_>| {
        if !with_snippet || crop_radius == 0 {
            return e;
        }
        match shared_ring.get_recent() {
            Ok(snapshot) => {
                let text = String::from_utf8_lossy(&snapshot.bytes);
                let root = crate::RootFragment {
                    text: text.as_ref(),
                    start_line: snapshot.start_line,
                    source_name: "input",
                };
                crate::maybe_with_snippet_from_events_and_root_fragment(
                    e,
                    Some(&root),
                    text.as_ref(),
                    src,
                    with_snippet,
                    crop_radius,
                )
            }
            Err(_) => e,
        }
    };

    let value = deserialize_with_scope(&mut src, cfg, f, wrap_err)?;
    enforce_single_document_and_finish(
        &mut src,
        "use read or read_with_options to obtain the iterator",
        wrap_err,
    )?;
    Ok(value)
}
