use camino::Utf8Path;
use libpijul::change::{Atom, BaseHunk, TextSerError};
use libpijul::changestore::ChangeStore;
use libpijul::pristine::sanakirja::{MutTxn, SanakirjaError};
use libpijul::working_copy::WorkingCopyRead;
use libpijul::{ArcTxn, ChannelRef, RecordBuilder};

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

#[derive(Debug, thiserror::Error)]
#[error(transparent)]
pub enum UnrecordedChangesError<C: std::error::Error + 'static, W: std::error::Error + 'static> {
    ChangeContents(#[from] TextSerError<C>),
    Globalize(#[from] SanakirjaError),
    Record(#[from] libpijul::record::RecordError<C, W, MutTxn<()>>),
}

#[derive(Clone, Debug)]
pub struct UnrecordedState {
    pub change_contents: Vec<u8>,
    pub hunks: Vec<BaseHunk<Atom<Option<libpijul::Hash>>, libpijul::change::Local>>,
    pub spans: Vec<Span<HunkOffset>>,
}

impl UnrecordedState {
    pub fn new<C, W>(
        path: &Utf8Path,
        transaction: &ArcTxn<MutTxn<()>>,
        channel: &ChannelRef<MutTxn<()>>,
        change_store: &C,
        // TODO: don't require a full working copy, use a `WorkingCopyRead` implementation that
        // only gives information on the current file. This is currently blocked on
        // `RecordBuilder::record()` seeming to ignore the full-file prefix and generating
        // invalid deletion hunks when the shim returns an error on unrelated files.
        working_copy: &W,
    ) -> Result<Self, UnrecordedChangesError<C::Error, W::Error>>
    where
        C: ChangeStore + Clone + Send + 'static,
        W: WorkingCopyRead + Clone + Send + Sync + 'static,
    {
        let mut unrecorded_changes = RecordBuilder::new();

        unrecorded_changes.record(
            transaction.clone(),
            libpijul::Algorithm::default(),
            false, // TODO: check and document
            &libpijul::DEFAULT_SEPARATOR,
            channel.clone(),
            working_copy,
            change_store,
            path.as_str(),
            1, // TODO: figure out concurrency model
        )?;

        let unrecorded_state = unrecorded_changes.finish();
        let change_contents = unrecorded_state.contents.lock();

        let mut unrecorded_spans = Vec::new();
        let mut hunks = Vec::with_capacity(unrecorded_state.actions.len());

        // TODO: handle encoding
        for hunk in unrecorded_state.actions {
            let globalized_hunk = hunk.globalize(&*transaction.read())?;
            debug_assert_eq!(globalized_hunk.path(), path.as_str());

            match &globalized_hunk {
                BaseHunk::Replacement {
                    change,
                    replacement,
                    local,
                    encoding,
                } => {
                    let old_contents = libpijul::change::get_change_contents(
                        change_store,
                        change,
                        &change_contents,
                    )?;
                    let new_contents = libpijul::change::get_change_contents(
                        change_store,
                        replacement,
                        &change_contents,
                    )?;

                    let old_text = String::from_utf8(old_contents).unwrap();
                    let new_text = String::from_utf8(new_contents).unwrap();

                    let old_line_count = old_text.lines().count().strict_sub(1);
                    let new_line_count = new_text.lines().count().strict_sub(1);

                    let span_start = local.line - 1;

                    unrecorded_spans.push(Span {
                        value: HunkOffset::Replacement {
                            old_line_count,
                            new_line_count,
                        },
                        lines: span_start..=span_start.strict_add(new_line_count),
                    });
                }
                BaseHunk::Edit {
                    change,
                    local,
                    encoding,
                } => {
                    let new_contents = libpijul::change::get_change_contents(
                        change_store,
                        change,
                        &change_contents,
                    )?;

                    let text = String::from_utf8(new_contents).unwrap();
                    let line_count = text.lines().count();

                    let span_start = local.line - 1;

                    let span = if let Atom::EdgeMap(edge) = change
                        && (edge.edges.is_empty() || edge.edges[0].flag.is_deleted())
                    {
                        Span {
                            value: HunkOffset::Deletion {
                                lines_removed: line_count,
                            },
                            lines: span_start..=span_start,
                        }
                    } else {
                        Span {
                            value: HunkOffset::Addition {
                                lines_added: line_count,
                            },
                            lines: span_start..=(span_start + line_count - 1),
                        }
                    };

                    unrecorded_spans.push(span);
                }
                // TODO: handle conflict resolution
                // TODO: handle moves
                _ => {
                    tracing::warn!(message = "Skipping unrecorded hunk", ?globalized_hunk);
                }
            }

            hunks.push(globalized_hunk);
        }

        unrecorded_spans.sort_by_key(|span| *span.lines.start());
        Ok(Self {
            change_contents: change_contents.clone(),
            hunks,
            spans: unrecorded_spans,
        })
    }
}