//! Gemeinsame Projekt-/Include-Auflösung für CLI und bearbeitete IDE-Quellen.
use std::{
    collections::BTreeMap,
    path::{Component, Path, PathBuf},
};
use tb_frontend::source::{SourceSegment, SourceUnit};
use tb_ui::frm::{self, FormFile};

pub fn module_name(path: &Path) -> String {
    path.file_stem()
        .map(|s| s.to_string_lossy().to_uppercase())
        .unwrap_or_else(|| "MODUL".into())
}

/// Absolute Identität, auch für noch nicht gespeicherte Dateien unter Symlink-Verzeichnissen.
pub fn identity(path: &Path) -> std::io::Result<PathBuf> {
    let absolute = std::path::absolute(path)?;
    let mut result = PathBuf::new();
    for part in absolute.components() {
        match part {
            Component::CurDir => {}
            Component::ParentDir => {
                result.pop();
            }
            _ => result.push(part.as_os_str()),
        }
        if let Ok(real) = result.canonicalize() {
            result = real;
        }
    }
    Ok(result)
}

pub fn has_extension(path: &Path, extension: &str) -> bool {
    path.extension()
        .is_some_and(|e| e.eq_ignore_ascii_case(extension))
}

#[derive(Debug, Clone, PartialEq)]
pub enum Content {
    Text(String),
    Form(Box<FormFile>),
}
impl Content {
    pub fn code(&self) -> &str {
        match self {
            Self::Text(s) => s,
            Self::Form(f) => &f.code,
        }
    }
    pub fn code_mut(&mut self) -> &mut String {
        match self {
            Self::Text(s) => s,
            Self::Form(f) => &mut f.code,
        }
    }
    pub fn text(&self) -> String {
        match self {
            Self::Text(s) => s.clone(),
            Self::Form(f) => frm::write_text(f),
        }
    }
}

#[derive(Debug)]
pub struct ReadDocument {
    pub content: Content,
    pub bytes: Vec<u8>,
    pub binary: bool,
    pub warnings: Vec<frm::BinaryWarning>,
}

pub fn read_document(path: &Path) -> Result<ReadDocument, String> {
    let bytes = std::fs::read(path).map_err(|e| format!("{}: {e}", path.display()))?;
    let binary = has_extension(path, "frm") && bytes.starts_with(&[0xfc, 0x08, 1, 0]);
    let mut warnings = Vec::new();
    let content = if binary {
        let read =
            frm::read_binary(&path.display().to_string(), &bytes).map_err(|e| e.to_string())?;
        warnings = read.skipped;
        Content::Form(Box::new(read.form))
    } else {
        let text = std::str::from_utf8(&bytes)
            .map_err(|e| format!("{}: ungültige Textkodierung: {e}", path.display()))?;
        if has_extension(path, "frm") {
            Content::Form(Box::new(
                frm::read_text(&path.display().to_string(), text).map_err(|e| e.to_string())?,
            ))
        } else {
            Content::Text(text.into())
        }
    };
    Ok(ReadDocument {
        content,
        bytes,
        binary,
        warnings,
    })
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ProjectLine {
    File(PathBuf),
    Comment(String),
    Blank,
}

#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Manifest {
    pub lines: Vec<ProjectLine>,
    pub startup: Option<PathBuf>,
}
impl Manifest {
    pub fn members(&self) -> impl Iterator<Item = &PathBuf> {
        self.lines.iter().filter_map(|l| match l {
            ProjectLine::File(p) => Some(p),
            _ => None,
        })
    }
    pub fn parse(path: &Path, text: &str, loader: &SourceLoader) -> Result<Self, String> {
        let base = path.parent().unwrap_or(Path::new("."));
        let mut out = Self::default();
        let mut startup = None;
        for (index, line) in text.lines().enumerate() {
            let s = line.trim();
            let fail = |msg: &str| format!("{}:{}: {msg}", path.display(), index + 1);
            if s.is_empty() {
                out.lines.push(ProjectLine::Blank);
            } else if let Some(comment) = s.strip_prefix('\'') {
                let comment = comment.trim_start();
                if comment.to_ascii_uppercase().starts_with("$STARTUP") {
                    if startup.is_some() {
                        return Err(fail("doppeltes $STARTUP"));
                    }
                    let value = comment[8..]
                        .trim_start()
                        .strip_prefix(':')
                        .map(str::trim)
                        .and_then(|s| s.strip_prefix('"'))
                        .and_then(|s| s.strip_suffix('"'))
                        .filter(|s| !s.is_empty() && !s.contains('"'))
                        .ok_or_else(|| fail("ungültiges $STARTUP"))?;
                    startup = Some(loader.resolve(base, value).map_err(|e| fail(&e))?);
                } else {
                    out.lines.push(ProjectLine::Comment(line.into()));
                }
            } else {
                let member = loader.resolve(base, s).map_err(|e| fail(&e))?;
                if !has_extension(&member, "bas") && !has_extension(&member, "frm") {
                    return Err(fail("Projektmitglied muss BAS oder FRM sein"));
                }
                let key = identity(&member).map_err(|e| fail(&e.to_string()))?;
                if out
                    .members()
                    .any(|p| identity(p).ok().as_ref() == Some(&key))
                {
                    return Err(fail("doppeltes Projektmitglied"));
                }
                out.lines.push(ProjectLine::File(member));
            }
        }
        if let Some(start) = startup {
            let key = identity(&start).map_err(|e| e.to_string())?;
            let chosen = out
                .members()
                .find(|p| identity(p).ok().as_ref() == Some(&key))
                .cloned()
                .ok_or_else(|| {
                    format!(
                        "{}: $STARTUP ist kein Projektmitglied: {}",
                        path.display(),
                        start.display()
                    )
                })?;
            out.startup = Some(chosen);
        }
        Ok(out)
    }
    pub fn text(&self, destination: &Path) -> Result<String, String> {
        let base = destination.parent().unwrap_or(Path::new("."));
        let mut out = String::new();
        if let Some(path) = &self.startup {
            let name = relative_path(base, path).map_err(|e| e.to_string())?;
            if name.contains(['"', '\n', '\r']) {
                return Err("$STARTUP-Pfad kann nicht dargestellt werden".into());
            }
            out.push_str(&format!("' $STARTUP: \"{name}\"\n"));
        }
        for line in &self.lines {
            match line {
                ProjectLine::File(path) => {
                    let name = relative_path(base, path).map_err(|e| e.to_string())?;
                    if name.contains(['\n', '\r']) || name.trim() != name || name.starts_with('\'')
                    {
                        return Err("MAK-Pfad kann nicht dargestellt werden".into());
                    }
                    out.push_str(&name);
                }
                ProjectLine::Comment(s) => out.push_str(s),
                ProjectLine::Blank => {}
            }
            out.push('\n');
        }
        Ok(out)
    }
    pub fn rename(&mut self, old: &Path, new: &Path) {
        for line in &mut self.lines {
            if let ProjectLine::File(p) = line {
                if p == old {
                    *p = new.into();
                }
            }
        }
        if self.startup.as_deref() == Some(old) {
            self.startup = Some(new.into());
        }
    }
}

pub fn relative_path(base: &Path, target: &Path) -> std::io::Result<String> {
    let base = identity(base)?;
    let target = identity(target)?;
    let a: Vec<_> = base.components().collect();
    let b: Vec<_> = target.components().collect();
    let common = a.iter().zip(&b).take_while(|(x, y)| x == y).count();
    if common == 0 {
        return target.to_str().map(str::to_owned).ok_or_else(|| {
            std::io::Error::new(
                std::io::ErrorKind::InvalidData,
                "Projektpfad ist nicht UTF-8",
            )
        });
    }
    let mut result = PathBuf::new();
    for _ in common..a.len() {
        result.push("..");
    }
    for c in &b[common..] {
        result.push(c.as_os_str());
    }
    result.to_str().map(str::to_owned).ok_or_else(|| {
        std::io::Error::new(
            std::io::ErrorKind::InvalidData,
            "Projektpfad ist nicht UTF-8",
        )
    })
}

#[derive(Debug, Default)]
pub struct SourceLoader {
    overlays: BTreeMap<PathBuf, Content>,
    pub include_paths: Vec<PathBuf>,
}

#[derive(Debug)]
pub struct ProjectSources {
    pub manifest: Manifest,
    pub units: Vec<SourceUnit>,
    pub forms: Vec<FormFile>,
}
impl ProjectSources {
    /// One startup contract for IDE, CLI and serialized bytecode.
    pub fn compile(
        &self,
        compiler: &mut crate::project::ProjectCompiler,
        name: &str,
    ) -> Result<crate::bytecode::CompiledModule, Vec<tb_frontend::Diagnostic>> {
        let mut catalog = tb_frontend::forms::FormCatalog::default();
        for form in &self.forms {
            catalog.append(&form.catalog());
        }
        let mut module = compiler.compile(name, &self.units, &catalog, &self.forms)?;
        if self
            .manifest
            .startup
            .as_ref()
            .is_some_and(|p| !has_extension(p, "frm"))
        {
            module.startup_form = None;
        }
        Ok(module)
    }
}

/// BASIC RUN resolves relative to the execution target, never to process cwd.
pub fn run_target(current: &Path, program: &str) -> Result<PathBuf, String> {
    let base = current.parent().unwrap_or(Path::new("."));
    if Path::new(program).extension().is_some() {
        return relative_case_insensitive(base, program)
            .and_then(|p| identity(&p))
            .map_err(|e| e.to_string());
    }
    for extension in ["bas", "frm", "mak", "tbc"] {
        if let Ok(path) = relative_case_insensitive(base, &format!("{program}.{extension}")) {
            return identity(&path).map_err(|e| e.to_string());
        }
    }
    Err(format!("{program}: Programm nicht gefunden"))
}

pub fn load_program(path: &Path) -> Result<crate::bytecode::CompiledModule, String> {
    if has_extension(path, "tbc") {
        let bytes = std::fs::read(path).map_err(|e| e.to_string())?;
        return crate::bytecode::CompiledModule::from_tbc(&bytes).map_err(|e| e.to_string());
    }
    SourceLoader::default()
        .load(path)?
        .compile(&mut Default::default(), &module_name(path))
        .map_err(|errors| {
            errors
                .iter()
                .map(ToString::to_string)
                .collect::<Vec<_>>()
                .join("\n")
        })
}

/// Fresh runtime for every start, restart and RUN in both frontends.
pub fn new_execution(
    module: crate::bytecode::CompiledModule,
    command: &str,
    size: Option<(usize, usize)>,
    line: Option<u32>,
) -> Result<crate::interp::Vm, tb_runtime::errors::RuntimeError> {
    let mut vm = crate::interp::Vm::new(module);
    if let Some(line) = line {
        vm.start_at_line(line)?;
    }
    if let Some((cols, rows)) = size {
        vm.rt.screen.resize(cols, rows);
        vm.forms.resize(cols, rows);
    }
    vm.rt.command = command.to_owned();
    vm.set_poll_interrupt(true);
    Ok(vm)
}

impl SourceLoader {
    pub fn insert(&mut self, path: &Path, content: Content) -> Result<(), String> {
        let key = identity(path).map_err(|e| e.to_string())?;
        self.overlays.insert(key, content);
        Ok(())
    }
    pub fn resolve(&self, base: &Path, relative: &str) -> Result<PathBuf, String> {
        let direct = base.join(relative);
        let key = identity(&direct).map_err(|e| e.to_string())?;
        if self.overlays.contains_key(&key) {
            return Ok(key);
        }
        if let Ok(path) = relative_case_insensitive(base, relative) {
            return Ok(path);
        }
        let matches: Vec<_> = self
            .overlays
            .keys()
            .filter(|p| {
                p.to_string_lossy()
                    .eq_ignore_ascii_case(&key.to_string_lossy())
            })
            .collect();
        match matches.as_slice() {
            [path] => Ok((*path).clone()),
            [] => Err(format!("{} nicht gefunden", direct.display())),
            _ => Err(format!("{} ist mehrdeutig", direct.display())),
        }
    }
    pub fn read(&self, path: &Path) -> Result<Content, String> {
        let key = identity(path).map_err(|e| e.to_string())?;
        self.overlays
            .get(&key)
            .cloned()
            .map(Ok)
            .unwrap_or_else(|| read_document(path).map(|d| d.content))
    }
    /// Relative Includes bei Save As auf dieselben Dateien richten. Nutztext bleibt unverändert.
    pub fn relocate(&self, content: &Content, from: &Path, to: &Path) -> Result<Content, String> {
        if from.parent() == to.parent() {
            return Ok(content.clone());
        }
        let old_base = from.parent().unwrap_or(Path::new("."));
        let new_base = to.parent().unwrap_or(Path::new("."));
        let mut text = String::new();
        for line in content.code().split_inclusive('\n') {
            if let Some((name, column)) = include_name(line) {
                if !name.is_empty() && !Path::new(&name).is_absolute() {
                    let target = self
                        .resolve_include(old_base, &name)
                        .unwrap_or_else(|_| old_base.join(&name));
                    let relative = relative_path(new_base, &target).map_err(|e| e.to_string())?;
                    if relative.contains(['\'', '\n', '\r']) {
                        return Err(format!("{}: Include-Pfad nicht darstellbar", to.display()));
                    }
                    let start = line
                        .char_indices()
                        .nth(column)
                        .map(|(i, _)| i)
                        .unwrap_or(line.len());
                    let comment = &line[start..];
                    // Der Lexer hat die gültige Include-Syntax bereits erkannt.
                    let quote = start + comment.find(':').unwrap() + 1;
                    let begin = quote + line[quote..].find('\'').unwrap() + 1;
                    let end = begin + line[begin..].find('\'').unwrap();
                    text.push_str(&line[..begin]);
                    text.push_str(&relative);
                    text.push_str(&line[end..]);
                    continue;
                }
            }
            text.push_str(line);
        }
        let mut result = content.clone();
        *result.code_mut() = text;
        Ok(result)
    }
    fn resolve_include(&self, base: &Path, name: &str) -> Result<PathBuf, String> {
        let mut found = self.resolve(base, name);
        if found.is_err() && !Path::new(name).is_absolute() {
            for base in &self.include_paths {
                found = self.resolve(base, name);
                if found.is_ok() {
                    break;
                }
            }
        }
        found
    }
    pub fn load(&self, path: &Path) -> Result<ProjectSources, String> {
        let path = self.resolve(Path::new("."), &path.to_string_lossy())?;
        let manifest = if has_extension(&path, "mak") {
            Manifest::parse(&path, &self.read(&path)?.text(), self)?
        } else {
            Manifest {
                lines: vec![ProjectLine::File(path)],
                startup: None,
            }
        };
        self.load_manifest(manifest)
    }
    pub fn load_manifest(&self, manifest: Manifest) -> Result<ProjectSources, String> {
        let mut units = Vec::new();
        let mut forms = Vec::new();
        let mut members: Vec<_> = manifest.members().collect();
        if let Some(startup) = &manifest.startup {
            let index = members
                .iter()
                .position(|p| *p == startup)
                .ok_or("Startdatei ist kein Projektmitglied")?;
            let selected = members.remove(index);
            members.insert(0, selected);
        }
        for path in members {
            let content = self.read(path)?;
            let (name, code, first_line) = match content {
                Content::Text(text) => (module_name(path), text, 1),
                Content::Form(form) => {
                    // Bei bearbeiteter Struktur müssen die physischen Codezeilen
                    // zur tatsächlich gespeicherten Textfassung passen.
                    let form = frm::read_text(&path.display().to_string(), &frm::write_text(&form))
                        .map_err(|e| e.to_string())?;
                    let result = (form.root.name.clone(), form.code.clone(), form.code_line());
                    forms.push(form);
                    result
                }
            };
            units.push(SourceUnit {
                name,
                segments: self.expand(path, &code, first_line, &mut Vec::new())?,
            });
        }
        Ok(ProjectSources {
            manifest,
            units,
            forms,
        })
    }
    fn expand(
        &self,
        path: &Path,
        text: &str,
        first_line: u32,
        stack: &mut Vec<PathBuf>,
    ) -> Result<Vec<SourceSegment>, String> {
        let key = identity(path).map_err(|e| e.to_string())?;
        if stack.contains(&key) {
            return Err(format!(
                "{}: zyklisches $INCLUDE: {} -> {}",
                path.display(),
                stack
                    .iter()
                    .map(|p| p.display().to_string())
                    .collect::<Vec<_>>()
                    .join(" -> "),
                key.display()
            ));
        }
        stack.push(key);
        let mut out = vec![SourceSegment {
            file: path.display().to_string(),
            first_line,
            text: String::new(),
        }];
        for (line_no, line) in text.split_inclusive('\n').enumerate() {
            if let Some((name, column)) = include_name(line) {
                let prefix: String = line.chars().take(column).collect();
                if !prefix.trim().is_empty() {
                    out.push(SourceSegment {
                        file: path.display().to_string(),
                        first_line: first_line + line_no as u32,
                        text: format!("{prefix}\n"),
                    });
                }
                if name.is_empty() {
                    return Err(format!(
                        "{}:{}: leeres $INCLUDE",
                        path.display(),
                        first_line + line_no as u32
                    ));
                }
                let found = self.resolve_include(path.parent().unwrap_or(Path::new(".")), &name);
                let included = found.map_err(|e| {
                    format!("{}:{}: {e}", path.display(), first_line + line_no as u32)
                })?;
                let source = self.read(&included)?.text();
                out.extend(self.expand(&included, &source, 1, stack)?);
            } else {
                out.push(SourceSegment {
                    file: path.display().to_string(),
                    first_line: first_line + line_no as u32,
                    text: line.into(),
                });
            }
        }
        stack.pop();
        Ok(out)
    }
}

fn include_name(line: &str) -> Option<(String, usize)> {
    if !line.to_ascii_uppercase().contains("$INCLUDE") {
        return None;
    }
    tb_frontend::lexer::lex(line)
        .tokens
        .into_iter()
        .find_map(|token| {
            if let tb_frontend::lexer::TokenKind::MetaInclude(name) = token.kind {
                Some((name, token.pos.column.saturating_sub(1) as usize))
            } else {
                None
            }
        })
}
pub fn relative_case_insensitive(base: &Path, relative: &str) -> std::io::Result<PathBuf> {
    let direct = base.join(relative);
    if direct.exists() {
        return Ok(direct);
    }
    let wanted = Path::new(relative);
    let mut current = identity(base)?;
    for component in wanted.components() {
        use std::path::Component;
        match component {
            Component::CurDir => {}
            Component::ParentDir => {
                current.pop();
            }
            Component::Normal(name) => {
                let entry = std::fs::read_dir(&current)?.find_map(|entry| {
                    let entry = entry.ok()?;
                    entry
                        .file_name()
                        .to_string_lossy()
                        .eq_ignore_ascii_case(&name.to_string_lossy())
                        .then(|| entry.path())
                });
                current = entry.ok_or_else(|| {
                    std::io::Error::new(
                        std::io::ErrorKind::NotFound,
                        format!("{} nicht gefunden", current.join(name).display()),
                    )
                })?;
            }
            Component::RootDir | Component::Prefix(_) => current.push(component.as_os_str()),
        }
    }
    Ok(current)
}
