use std::fs;
use std::path::{Path, PathBuf};
use std::process::ExitCode;

use anyhow::{Context, Result, bail};

use crate::SyncDepsArgs;
use crate::overlay::{self, Layer};
use crate::registry::{self, Registry};
use crate::render::{REGION_END, REGION_START, Rebase, render_region};

/// Outcome of a single-target sync.
enum SyncOutcome {
    UpToDate,
    Synced,
    Drift,
}

pub fn run(args: SyncDepsArgs) -> Result<ExitCode> {
    if args.all && args.overlay.is_some() {
        bail!(
            "--overlay cannot be combined with --all (each impl uses its own auto-detected overlay)"
        );
    }
    if args.all && args.target.is_some() {
        bail!("--target cannot be combined with --all");
    }

    let registry = registry::load(&args.rustelo_root)?;

    let targets = resolve_targets(&args, &registry)?;

    let mut drifted = false;
    for target in targets {
        let outcome = sync_one(
            &args.rustelo_root,
            &target,
            args.overlay.as_deref(),
            &registry,
            args.check,
        )?;
        let cargo_path = target.join("Cargo.toml");
        match outcome {
            SyncOutcome::UpToDate => {
                println!("sync-deps: {} is up to date", cargo_path.display());
            }
            SyncOutcome::Synced => {
                println!("sync-deps: wrote {}", cargo_path.display());
            }
            SyncOutcome::Drift => {
                eprintln!(
                    "sync-deps: drift detected in {} — run `cargo xtask sync-deps` to fix",
                    cargo_path.display()
                );
                drifted = true;
            }
        }
    }

    Ok(if drifted {
        ExitCode::from(1)
    } else {
        ExitCode::SUCCESS
    })
}

/// Builds the ordered list of target directories to sync.
///
/// - `--all`: rustelo workspace root + every path listed in
///   `registry/sync.toml` `[impls.known]` (resolved relative to the
///   rustelo root). Each impl path is validated to contain a `Cargo.toml`.
/// - Otherwise: just `--target` (defaults to ".").
fn resolve_targets(args: &SyncDepsArgs, _registry: &Registry) -> Result<Vec<PathBuf>> {
    if args.all {
        let sidecar = registry::load_sidecar(&args.rustelo_root)?;
        let mut targets = Vec::with_capacity(1 + sidecar.known_impls.len());
        targets.push(args.rustelo_root.clone());
        for rel in &sidecar.known_impls {
            let abs = args.rustelo_root.join(rel);
            let cargo = abs.join("Cargo.toml");
            if !cargo.exists() {
                bail!(
                    "registry/sync.toml [impls.known] points at `{}` but {} does not exist",
                    rel,
                    cargo.display()
                );
            }
            targets.push(abs);
        }
        Ok(targets)
    } else {
        let target = args.target.clone().unwrap_or_else(|| PathBuf::from("."));
        Ok(vec![target])
    }
}

/// Syncs (or checks) one target. The overlay path can override auto-detection
/// of `{target}/rustelo-deps-overlay.toml`.
fn sync_one(
    rustelo_root: &Path,
    target_dir: &Path,
    overlay_path: Option<&Path>,
    registry: &Registry,
    check: bool,
) -> Result<SyncOutcome> {
    let rebase = Rebase::new(rustelo_root, target_dir)?;

    let layers = load_chain(target_dir, overlay_path)?;
    overlay::validate_chain(&layers, registry)?;

    let cargo_path = target_dir.join("Cargo.toml");
    let original = fs::read_to_string(&cargo_path)
        .with_context(|| format!("reading {}", cargo_path.display()))?;

    let rendered_region = render_region(registry, &layers, &rebase);
    let new_contents = splice_region(&original, &rendered_region, &cargo_path)?;

    if new_contents == original {
        return Ok(SyncOutcome::UpToDate);
    }

    if check {
        return Ok(SyncOutcome::Drift);
    }

    fs::write(&cargo_path, &new_contents)
        .with_context(|| format!("writing {}", cargo_path.display()))?;
    Ok(SyncOutcome::Synced)
}

/// Builds the ordered overlay chain (parent→child) for a target. With an
/// explicit `--overlay` path the chain is that single file — the escape hatch
/// stays deliberately flat and does not follow `extends`. Otherwise the chain is
/// auto-resolved from `{target}/rustelo-deps-overlay.toml` and its `extends`
/// links.
fn load_chain(target_dir: &Path, explicit: Option<&Path>) -> Result<Vec<Layer>> {
    match explicit {
        Some(path) => {
            let overlay = overlay::load_file(path)?;
            let dir = path
                .parent()
                .map(Path::to_path_buf)
                .unwrap_or_else(|| PathBuf::from("."));
            Ok(vec![Layer { overlay, dir }])
        }
        None => overlay::load_chain(target_dir),
    }
}

/// Replaces the managed region inside [workspace.dependencies] with the rendered text.
///
/// If markers are absent, the region is inserted at the top of the
/// [workspace.dependencies] table (preserving any manually-curated lines below
/// the end marker — those will live outside the managed region).
fn splice_region(original: &str, region: &str, cargo_path: &Path) -> Result<String> {
    if let (Some(start), Some(end)) = (original.find(REGION_START), original.find(REGION_END)) {
        if end < start {
            bail!(
                "{}: end marker appears before start marker",
                cargo_path.display()
            );
        }
        let end_full = end + REGION_END.len();
        let mut out = String::with_capacity(original.len());
        out.push_str(&original[..start]);
        out.push_str(region);
        out.push_str(&original[end_full..]);
        return Ok(out);
    }

    let Some(hdr_idx) = find_table_header(original, "[workspace.dependencies]") else {
        bail!(
            "{}: no [workspace.dependencies] section found and no existing sync markers",
            cargo_path.display()
        );
    };
    let body_start = original[hdr_idx..]
        .find('\n')
        .map(|n| hdr_idx + n + 1)
        .unwrap_or(original.len());

    let body_end = next_table_header(&original[body_start..])
        .map(|off| body_start + off)
        .unwrap_or(original.len());

    let mut out = String::with_capacity(original.len() + region.len());
    out.push_str(&original[..body_start]);
    out.push_str(region);
    out.push('\n');
    out.push('\n');
    out.push_str(&original[body_end..]);
    Ok(out)
}

/// Finds the offset of a specific table header (e.g. `[workspace.dependencies]`),
/// requiring it to start at column 0 of a line. Avoids false matches inside
/// comments or strings that mention the bracketed phrase.
fn find_table_header(text: &str, header: &str) -> Option<usize> {
    let mut offset = 0;
    for line in text.split_inclusive('\n') {
        let body = line.trim_end_matches('\n').trim_end_matches('\r');
        if body == header {
            return Some(offset);
        }
        offset += line.len();
    }
    None
}

/// Finds the offset of the next top-level table header line (a line starting with `[`
/// at column 0) within `text`. Returns None if no such header exists.
fn next_table_header(text: &str) -> Option<usize> {
    let mut offset = 0;
    for line in text.split_inclusive('\n') {
        let trimmed_start = line.trim_start_matches([' ', '\t']);
        if trimmed_start.starts_with('[') && trimmed_start == line {
            return Some(offset);
        }
        offset += line.len();
    }
    None
}
