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

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

use super::link::{self, LinkRequest};
use crate::output::CommandReport;
use crate::paths::state::StateLayout;
use crate::paths::substrate::SubstrateKind;
use crate::profile;
use crate::repo::marker;
use crate::repo::registry::{self as repo_registry, RepoAliases, RepoRegistryEntry};

#[derive(Serialize)]
pub struct AttachReport {
    command: &'static str,
    ok: bool,
    path: String,
    profile: String,
    locality_id: String,
    action: &'static str,
    resolution: &'static str,
    profile_root: String,
    clone_profile_root: String,
    marker_path: String,
    registry_path: String,
    overlay_path: String,
    created_profile: bool,
    created_clone_profile: bool,
    created_kernel_files: Vec<String>,
    warnings: Vec<String>,
    next_steps: Vec<String>,
}

impl CommandReport for AttachReport {
    fn exit_code(&self) -> ExitCode {
        ExitCode::SUCCESS
    }

    fn render_text(&self) {
        if self.action == "already_attached" {
            println!(
                "Already attached to profile {} (project ID {}; locality_id compatibility). No changes needed.",
                self.profile, self.locality_id
            );
            return;
        }

        println!(
            "Initialized CCD state for profile {} with project ID {} ({}, {}).",
            self.profile, self.locality_id, self.action, self.resolution
        );
        println!("Profile root: {}", self.profile_root);
        println!("Workspace state: {}", self.clone_profile_root);
        println!("Marker: {}", self.marker_path);
        println!("Registry: {}", self.registry_path);
        println!("Project overlay: {}", self.overlay_path);

        for warning in &self.warnings {
            println!("Warning: {warning}");
        }

        if self.next_steps.is_empty() {
            return;
        }

        println!();
        println!("Next steps:");
        for (index, step) in self.next_steps.iter().enumerate() {
            println!("{}. {step}", index + 1);
        }
    }
}

impl AttachReport {
    pub(crate) fn profile_name(&self) -> &str {
        &self.profile
    }

    pub(crate) fn locality_id(&self) -> &str {
        &self.locality_id
    }
}

pub fn run(
    target_dir: &Path,
    explicit_profile: Option<&str>,
    explicit_locality_id: Option<&str>,
    display_name: Option<&str>,
) -> Result<AttachReport> {
    validate_target(target_dir)?;
    let profile_name = profile::resolve(explicit_profile)?;
    let layout = StateLayout::resolve_for_attach(target_dir, profile_name.clone())?;
    let existing_marker = marker::load(target_dir)?;

    if explicit_profile.is_none() && explicit_locality_id.is_none() && display_name.is_none() {
        if let Some(existing_marker) = existing_marker.as_ref() {
            if layout
                .repo_registry_dir(&existing_marker.locality_id)?
                .is_dir()
                && (layout.resolved_substrate().kind() != SubstrateKind::Directory
                    || layout.resolved_substrate().workspace_binding_exists())
            {
                return already_attached_report(
                    target_dir,
                    &profile_name,
                    &layout,
                    existing_marker,
                );
            }
        }
    }

    match layout.resolved_substrate().kind() {
        SubstrateKind::Git => run_git_attach(
            target_dir,
            explicit_profile,
            explicit_locality_id,
            display_name,
        ),
        SubstrateKind::Directory => run_directory_attach(
            target_dir,
            profile_name,
            layout,
            existing_marker,
            explicit_locality_id,
            display_name,
        ),
    }
}

fn already_attached_report(
    target_dir: &Path,
    profile_name: &profile::ProfileName,
    layout: &StateLayout,
    existing_marker: &marker::RepoMarker,
) -> Result<AttachReport> {
    Ok(AttachReport {
        command: "attach",
        ok: true,
        path: target_dir.display().to_string(),
        profile: profile_name.to_string(),
        locality_id: existing_marker.locality_id.clone(),
        action: "already_attached",
        resolution: "no_change",
        profile_root: layout.profile_root().display().to_string(),
        clone_profile_root: layout.clone_profile_root().display().to_string(),
        marker_path: target_dir.join(marker::MARKER_FILE).display().to_string(),
        registry_path: layout
            .repo_metadata_path(&existing_marker.locality_id)?
            .display()
            .to_string(),
        overlay_path: layout
            .repo_overlay_root(&existing_marker.locality_id)?
            .display()
            .to_string(),
        created_profile: false,
        created_clone_profile: false,
        created_kernel_files: vec![],
        warnings: vec![],
        next_steps: vec![],
    })
}

fn run_git_attach(
    target_dir: &Path,
    explicit_profile: Option<&str>,
    explicit_locality_id: Option<&str>,
    display_name: Option<&str>,
) -> Result<AttachReport> {
    let prepared = link::prepare_link(
        target_dir,
        LinkRequest {
            explicit_profile,
            explicit_locality_id,
            display_name,
        },
    )?;

    let created_profile = !prepared.layout.profile_root().is_dir();
    let clone_profile_root = prepared.layout.clone_profile_root();
    let created_clone_profile = !clone_profile_root.is_dir();

    let profile_root = profile::ensure_profile_dir(prepared.layout.ccd_root(), &prepared.profile)?;
    let created_kernel_files = ensure_profile_kernel_files(&prepared.layout)?;
    fs::create_dir_all(&clone_profile_root).with_context(|| {
        format!(
            "failed to create directory {}",
            clone_profile_root.display()
        )
    })?;

    let link_result = link::finalize_link(prepared)?;

    Ok(AttachReport {
        command: "attach",
        ok: true,
        path: target_dir.display().to_string(),
        profile: link_result.profile.to_string(),
        locality_id: link_result.locality_id.clone(),
        action: link_result.action,
        resolution: link_result.resolution,
        profile_root: profile_root.display().to_string(),
        clone_profile_root: clone_profile_root.display().to_string(),
        marker_path: link_result.marker_path.display().to_string(),
        registry_path: link_result.registry_path.display().to_string(),
        overlay_path: link_result.overlay_path.display().to_string(),
        created_profile,
        created_clone_profile,
        created_kernel_files: created_kernel_files
            .iter()
            .map(|path| path.display().to_string())
            .collect(),
        warnings: link_result.warnings,
        next_steps: vec![
            format!(
                "Run `ccd start --activate --path {} --profile {}` when you begin work in this workspace.",
                target_dir.display(),
                link_result.profile
            ),
            format!(
                "Use `ccd link --path <workspace> --project-id {}` to attach another workspace to the same project overlay.",
                link_result.locality_id
            ),
        ],
    })
}

fn run_directory_attach(
    target_dir: &Path,
    profile_name: profile::ProfileName,
    layout: StateLayout,
    existing_marker: Option<marker::RepoMarker>,
    explicit_locality_id: Option<&str>,
    display_name: Option<&str>,
) -> Result<AttachReport> {
    let mut warnings = Vec::new();
    let chosen_display_name = link::choose_display_name(
        display_name,
        existing_marker
            .as_ref()
            .and_then(|marker| marker.display_name.as_deref()),
        target_dir.file_name().and_then(|name| name.to_str()),
    );

    let (locality_id, action, resolution, previous_locality_id) = if let Some(locality_id) =
        explicit_locality_id
    {
        if let Some(existing_marker) = existing_marker.as_ref() {
            if existing_marker.locality_id != locality_id {
                warnings.push(format!(
                    "relinked directory clone from locality_id `{}` to explicit locality_id `{locality_id}`",
                    existing_marker.locality_id
                ));
            }
        }
        (
            locality_id.to_owned(),
            "attached",
            "explicit_locality_id",
            existing_marker
                .as_ref()
                .map(|marker| marker.locality_id.clone())
                .filter(|previous| previous != locality_id),
        )
    } else if let Some(existing_marker) = existing_marker.as_ref() {
        if !layout.resolved_substrate().workspace_binding_exists() {
            warnings.push(
                "directory workspace state was not initialized for this canonical path; creating fresh local workspace state"
                    .to_owned(),
            );
        }
        (
            existing_marker.locality_id.clone(),
            "refreshed",
            "local_marker",
            None,
        )
    } else {
        (
            link::generate_locality_id(target_dir, &layout)?,
            "created",
            "new_assignment",
            None,
        )
    };

    let marker = match existing_marker.as_ref() {
        Some(existing_marker) => existing_marker
            .rewrite_with_locality_id(locality_id.clone(), chosen_display_name.clone())?,
        None => {
            marker::RepoMarker::new_directory(locality_id.clone(), chosen_display_name.clone())?
        }
    };

    let current_clone_root = repo_registry::normalize_clone_root(target_dir)?;
    let registry_path = layout.repo_metadata_path(&locality_id)?;
    let existing_entry = repo_registry::load(&registry_path)?;
    let mut known_clone_roots = existing_entry
        .as_ref()
        .map(|entry| entry.known_clone_roots.clone())
        .unwrap_or_default();
    if !known_clone_roots
        .iter()
        .any(|known| known == &current_clone_root)
    {
        known_clone_roots.push(current_clone_root.clone());
        known_clone_roots.sort();
    }
    let registry_entry = RepoRegistryEntry::new_with_known_clone_roots(
        link::choose_display_name(
            display_name,
            existing_entry
                .as_ref()
                .and_then(|entry| entry.display_name.as_deref())
                .or(chosen_display_name.as_deref()),
            target_dir.file_name().and_then(|name| name.to_str()),
        ),
        directory_aliases(target_dir),
        known_clone_roots,
    )?;

    let created_profile = !layout.profile_root().is_dir();
    let clone_profile_root = layout.clone_profile_root();
    let created_clone_profile = !clone_profile_root.is_dir();
    let profile_root = profile::ensure_profile_dir(layout.ccd_root(), &profile_name)?;
    let created_kernel_files = ensure_profile_kernel_files(&layout)?;
    layout.resolved_substrate().ensure_workspace_binding()?;
    fs::create_dir_all(&clone_profile_root).with_context(|| {
        format!(
            "failed to create directory {}",
            clone_profile_root.display()
        )
    })?;

    let marker_path = marker::write(target_dir, &marker)?;
    let registry_path = repo_registry::write(&registry_path, &registry_entry)?;
    if let Some(previous_locality_id) = previous_locality_id.as_deref() {
        if let Err(error) =
            link::remove_known_clone_root(&layout, previous_locality_id, &current_clone_root)
        {
            warnings.push(format!(
                "failed to prune clone root {current_clone_root} from previous locality_id `{previous_locality_id}`: {error:#}",
            ));
        }
    }
    let overlay_path = layout.repo_overlay_root(&locality_id)?;
    fs::create_dir_all(&overlay_path)
        .with_context(|| format!("failed to create directory {}", overlay_path.display()))?;

    Ok(AttachReport {
        command: "attach",
        ok: true,
        path: target_dir.display().to_string(),
        profile: profile_name.to_string(),
        locality_id: locality_id.clone(),
        action,
        resolution,
        profile_root: profile_root.display().to_string(),
        clone_profile_root: clone_profile_root.display().to_string(),
        marker_path: marker_path.display().to_string(),
        registry_path: registry_path.display().to_string(),
        overlay_path: overlay_path.display().to_string(),
        created_profile,
        created_clone_profile,
        created_kernel_files: created_kernel_files
            .iter()
            .map(|path| path.display().to_string())
            .collect(),
        warnings,
        next_steps: vec![
            format!(
                "Run `ccd start --activate --path {} --profile {}` when you begin work in this directory.",
                target_dir.display(),
                profile_name
            ),
            format!(
                "Use `ccd attach --path <directory> --project-id {locality_id}` to attach another directory workspace to the same project overlay.",
            ),
        ],
    })
}

fn directory_aliases(target_dir: &Path) -> RepoAliases {
    RepoAliases {
        remote_urls: Vec::new(),
        root_commits: Vec::new(),
        repo_basenames: target_dir
            .file_name()
            .and_then(|name| name.to_str())
            .map(|name| vec![name.to_owned()])
            .unwrap_or_default(),
    }
}

fn validate_target(target_dir: &Path) -> Result<()> {
    if !target_dir.exists() {
        bail!("target directory does not exist: {}", target_dir.display());
    }

    if !target_dir.is_dir() {
        bail!("target path is not a directory: {}", target_dir.display());
    }

    Ok(())
}

fn ensure_profile_kernel_files(layout: &StateLayout) -> Result<Vec<PathBuf>> {
    let files = vec![
        layout.profile_config_path(),
        layout.profile_policy_path(),
        layout.profile_memory_path(),
    ];
    let mut created = Vec::new();

    for path in files {
        if path.exists() {
            continue;
        }

        fs::write(&path, "").with_context(|| format!("failed to write {}", path.display()))?;
        created.push(path);
    }

    Ok(created)
}
