use std::ops::RangeInclusive;
use std::sync::OnceLock;

use camino::Utf8Path;
use libpijul::changestore::ChangeStore;
use libpijul::pristine::sanakirja::{MutTxn, SanakirjaError};
use libpijul::vertex_buffer::VertexBuffer;
use libpijul::{ArcTxn, ChangeId, ChannelRef, TxnTExt, Vertex};

use crate::file_system::changes::{CreditSource, Span};

#[derive(Debug, thiserror::Error)]
pub enum RecordedStateError<C: std::error::Error + 'static> {
    #[error("unable to find oldest vertex: {0:#?}")]
    OldestVertex(#[from] libpijul::fs::FsErrorC<C, MutTxn<()>>),
    #[error("unable to output file: {0:#?}")]
    OutputFile(#[from] libpijul::output::FileError<C, MutTxn<()>>),
    #[error("unable to get external hash for {change_id:#?}")]
    ExternalHash {
        change_id: libpijul::pristine::ChangeId,
        error: libpijul::pristine::TxnErr<SanakirjaError>,
    },
    #[error("no matching external hash for change ID: {0:#?}")]
    MissingExternalHash(libpijul::pristine::ChangeId),
    #[error("unable to get hash from changestore: {0:#?}")]
    ChangeStore(C),
}

// TODO: reference diff::vertex_buffer::Diff file:///home/finchie/Projects/Pijul/pijul/target/doc/libpijul/diff/vertex_buffer/struct.Diff.html
struct ChangesBuffer {
    introduced_by: OnceLock<ChangeId>,
    original_contents: String,
    spans: Vec<Span<Vertex<ChangeId>>>,
}

// TODO: don't assume encoding
#[derive(Clone, Debug)]
pub struct RecordedState {
    pub introduced_by: ChangeId,
    pub original_contents: String,
    pub spans: Vec<Span<Vertex<ChangeId>>>,
}

impl RecordedState {
    pub fn new<C>(
        path: &Utf8Path,
        transaction: &ArcTxn<MutTxn<()>>,
        channel: &ChannelRef<MutTxn<()>>,
        change_store: &C,
    ) -> Result<Self, RecordedStateError<C::Error>>
    where
        C: ChangeStore,
    {
        // TODO: handle ambiguity (possibly when there is a path conflict?)
        let (oldest_vertex, _ambiguous) =
            transaction
                .read()
                .follow_oldest_path(change_store, channel, path.as_str())?;

        let mut changes_buffer = ChangesBuffer {
            introduced_by: OnceLock::new(),
            original_contents: String::new(),
            spans: Vec::new(),
        };
        libpijul::output::output_file(
            change_store,
            transaction,
            channel,
            oldest_vertex,
            &mut changes_buffer,
        )?;

        Ok(Self {
            introduced_by: changes_buffer.introduced_by.take().unwrap(),
            original_contents: changes_buffer.original_contents,
            spans: changes_buffer.spans,
        })
    }

    pub fn spans_within_lines(
        &self,
        untracked_line_range: RangeInclusive<usize>,
        offset: isize,
    ) -> impl Iterator<Item = Span<CreditSource>> {
        let tracked_line_range = untracked_line_range.start().strict_add_signed(offset)
            ..=untracked_line_range.end().strict_add_signed(offset);

        Span::spans_within_lines(&self.spans, tracked_line_range).map(move |span| Span {
            value: CreditSource::Tracked { vertex: span.value },
            lines: (span.lines.start().strict_sub_signed(offset))
                ..=(span.lines.end().strict_sub_signed(offset)),
        })
    }
}

impl VertexBuffer for ChangesBuffer {
    fn output_line<E, F>(&mut self, vertex: Vertex<ChangeId>, contents: F) -> Result<(), E>
    where
        E: From<std::io::Error>,
        F: FnOnce(&mut [u8]) -> Result<(), E>,
    {
        // The first "line" output is an empty line representing the change that introduced the file
        if self.introduced_by.get().is_none() {
            self.introduced_by.set(vertex.change).unwrap();
            assert!(vertex.is_empty());

            return Ok(());
        }

        // The final "line" output is an empty line with an empty change ID
        if vertex.change.0.as_u64() == 0 {
            return Ok(());
        }

        assert!(!vertex.is_empty());
        assert!(!vertex.is_root());

        let mut buffer = vec![0; vertex.end - vertex.start];
        contents(&mut buffer)?;

        let contents = String::from_utf8(buffer).unwrap();

        // Keep track of the original contents
        self.original_contents.push_str(&contents);

        // Add the starting line and line count of the contents affected by this change ID
        let start_line = match self.spans.last() {
            Some(previous_span) => previous_span.next_span_start(),
            None => 0,
        };
        let length = contents.lines().count();

        self.spans.push(Span {
            value: vertex,
            lines: start_line..=(start_line + length - 1),
        });

        Ok(())
    }

    // TODO: handle all the conflict markers independently
    fn output_conflict_marker<C: libpijul::changestore::ChangeStore>(
        &mut self,
        s: &str,
        id: usize,
        sides: Option<(&C, &[&libpijul::Hash])>,
    ) -> Result<(), std::io::Error> {
        todo!("Conflict markers")
    }
}