use std::path::Path;
use std::process::ExitCode;

use anyhow::Result;
use serde::Serialize;

use crate::output::CommandReport;
use crate::paths::state::StateLayout;
use crate::profile::{self, ProfileName, DEFAULT_PROFILE};
use crate::repo::marker as repo_marker;
use crate::repo::registry as repo_registry;
use crate::repo::truth as project_truth;

#[derive(Serialize)]
pub struct PreflightReport {
    command: &'static str,
    ok: bool,
    path: String,
    profile: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    project_id: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    locality_id: Option<String>,
    readiness: ReadinessView,
    source_order: SourceOrderView,
    entrypoints: Vec<EntrypointView>,
    session_sequence: Vec<SessionStepView>,
    warnings: Vec<String>,
}

#[derive(Serialize)]
struct ReadinessView {
    status: &'static str,
    blockers: Vec<String>,
}

#[derive(Serialize)]
struct SourceOrderView {
    status: &'static str,
    mode: &'static str,
    #[serde(skip_serializing_if = "Option::is_none")]
    manifest_path: Option<String>,
    manifest_status: &'static str,
    entries: Vec<String>,
    active_sources: Vec<String>,
    note: String,
}

#[derive(Serialize)]
struct EntrypointView {
    name: &'static str,
    role: &'static str,
    path: String,
    required: bool,
    status: &'static str,
    note: &'static str,
}

#[derive(Serialize)]
struct SessionStepView {
    command: String,
    status: &'static str,
    reason: String,
}

struct EntrypointPaths<'a> {
    agents: &'a Path,
    repo_memory: &'a Path,
    readme: &'a Path,
    marker: &'a Path,
    profile_config: &'a Path,
    profile_policy: &'a Path,
    profile_memory: &'a Path,
    registry: Option<&'a Path>,
    registry_ready: bool,
}

impl CommandReport for PreflightReport {
    fn exit_code(&self) -> ExitCode {
        if self.ok {
            ExitCode::SUCCESS
        } else {
            ExitCode::from(1)
        }
    }

    fn render_text(&self) {
        println!(
            "Preflight is {} for profile `{}`.",
            self.readiness.status, self.profile
        );
        if let Some(project_id) = self.project_id.as_ref().or(self.locality_id.as_ref()) {
            println!("Project ID: {project_id}");
        }
        println!(
            "Source order: {} ({})",
            self.source_order.mode, self.source_order.status
        );
        println!("{}", self.source_order.note);

        if !self.readiness.blockers.is_empty() {
            println!();
            println!("Blockers:");
            for blocker in &self.readiness.blockers {
                println!("- {blocker}");
            }
        }

        if !self.warnings.is_empty() {
            println!();
            println!("Warnings:");
            for warning in &self.warnings {
                println!("- {warning}");
            }
        }

        println!();
        println!("Recommended session-start sequence:");
        for (index, step) in self.session_sequence.iter().enumerate() {
            println!("{}. [{}] {}", index + 1, step.status, step.command);
            println!("   {}", step.reason);
        }
    }
}

pub fn run(repo_root: &Path, explicit_profile: Option<&str>) -> Result<PreflightReport> {
    let profile = profile::resolve(explicit_profile)?;
    let layout = StateLayout::resolve(repo_root, profile.clone())?;
    let marker = repo_marker::load(repo_root)?;
    let locality_id = marker.as_ref().map(|value| value.locality_id.clone());

    let agents_path = repo_root.join("AGENTS.md");
    let repo_memory_path = repo_root.join("MEMORY.md");
    let readme_path = repo_root.join("README.md");
    let marker_path = repo_root.join(repo_marker::MARKER_FILE);
    let profile_config_path = layout.profile_config_path();
    let profile_policy_path = layout.profile_policy_path();
    let profile_memory_path = layout.profile_memory_path();

    let profile_kernel_ready = profile_config_path.is_file()
        && profile_policy_path.is_file()
        && profile_memory_path.is_file();
    let registry_path = locality_id
        .as_deref()
        .map(|value| layout.repo_metadata_path(value))
        .transpose()?;
    let registry_ready = match &registry_path {
        Some(path) => repo_registry::load(path)?.is_some(),
        None => false,
    };

    let entrypoints = build_entrypoints(&EntrypointPaths {
        agents: &agents_path,
        repo_memory: &repo_memory_path,
        readme: &readme_path,
        marker: &marker_path,
        profile_config: &profile_config_path,
        profile_policy: &profile_policy_path,
        profile_memory: &profile_memory_path,
        registry: registry_path.as_deref(),
        registry_ready,
    });

    let mut blockers = Vec::new();
    if !agents_path.is_file() {
        blockers.push(format!(
            "{} is missing; seed canonical project-truth entrypoints with `ccd scaffold --path .`",
            agents_path.display()
        ));
    }
    if !profile_kernel_ready {
        blockers.push(format!(
            "profile `{}` is not bootstrapped at {}; run `ccd attach --path .` before opening a session",
            profile,
            layout.profile_root().display()
        ));
    }
    if marker.is_none() {
        blockers.push(format!(
            "{} is missing; run `ccd attach --path .` or `ccd link --path .` before opening a session",
            marker_path.display()
        ));
    }
    if marker.is_some() && !registry_ready {
        let path = registry_path
            .as_ref()
            .expect("registry path is available when marker exists");
        let linked_locality_id = locality_id
            .as_deref()
            .expect("project id is available when marker exists");
        blockers.push(format!(
            "project ID `{linked_locality_id}` is not linked in the registry at {}; run `ccd link --path . --project-id {linked_locality_id}` before opening a session",
            path.display()
        ));
    }

    let mut warnings = Vec::new();
    for (path, label) in [
        (&repo_memory_path, "project memory"),
        (&readme_path, "README"),
    ] {
        if !path.is_file() {
            warnings.push(format!(
                "{} is missing at {}; the session can still start, but context will be thinner",
                label,
                path.display()
            ));
        }
    }

    let source_order = resolve_source_order(
        repo_root,
        &layout,
        locality_id.as_deref(),
        profile_kernel_ready,
        registry_ready,
    )?;
    if profile_kernel_ready && registry_ready {
        if let Some(locality_id) = locality_id.as_deref() {
            for diagnostic in
                crate::extensions::health_diagnostics(&layout, repo_root, locality_id)?
            {
                warnings.push(diagnostic.message);
            }
            if let Some(message) =
                project_truth::legacy_roadmap_exclusion_warning(repo_root, &layout, locality_id)?
            {
                warnings.push(message);
            }
        }
    }
    let session_sequence = build_session_sequence(
        &profile,
        explicit_profile,
        !agents_path.is_file(),
        !profile_kernel_ready || marker.is_none(),
        marker.is_some() && !registry_ready,
        locality_id.as_deref(),
    );

    Ok(PreflightReport {
        command: "preflight",
        ok: blockers.is_empty(),
        path: repo_root.display().to_string(),
        profile: profile.to_string(),
        project_id: locality_id.clone(),
        locality_id,
        readiness: ReadinessView {
            status: if blockers.is_empty() {
                "ready"
            } else {
                "blocked"
            },
            blockers,
        },
        source_order,
        entrypoints,
        session_sequence,
        warnings,
    })
}

fn build_entrypoints(paths: &EntrypointPaths<'_>) -> Vec<EntrypointView> {
    let mut entrypoints = vec![
        file_entrypoint(
            "AGENTS.md",
            "project_policy",
            paths.agents,
            true,
            "Canonical repo policy for AI agents.",
        ),
        file_entrypoint(
            "MEMORY.md",
            "project_memory",
            paths.repo_memory,
            false,
            "Optional repo-local memory surfaced during session start.",
        ),
        file_entrypoint(
            "README.md",
            "project_readme",
            paths.readme,
            false,
            "Optional repo overview surfaced by the default project-truth order.",
        ),
        file_entrypoint(
            ".ccd.toml",
            "repo_marker",
            paths.marker,
            true,
            "Workspace-local marker that links this workspace to a project ID.",
        ),
        file_entrypoint(
            "config.toml",
            "profile_config",
            paths.profile_config,
            true,
            "Profile kernel config required for CCD commands.",
        ),
        file_entrypoint(
            "policy.md",
            "profile_policy",
            paths.profile_policy,
            true,
            "Profile kernel policy layer.",
        ),
        file_entrypoint(
            "memory.md",
            "profile_memory",
            paths.profile_memory,
            true,
            "Profile kernel memory layer.",
        ),
    ];

    entrypoints.push(match paths.registry {
        Some(path) => EntrypointView {
            name: "repo.toml",
            role: "repo_registry",
            path: path.display().to_string(),
            required: true,
            status: if paths.registry_ready {
                "loaded"
            } else {
                "missing"
            },
            note: "Project registry entry that confirms the project ID is linked.",
        },
        None => EntrypointView {
            name: "repo.toml",
            role: "repo_registry",
            path: String::new(),
            required: true,
            status: "blocked",
            note: "Project registry path is unknown until .ccd.toml is present.",
        },
    });

    entrypoints
}

fn file_entrypoint(
    name: &'static str,
    role: &'static str,
    path: &Path,
    required: bool,
    note: &'static str,
) -> EntrypointView {
    EntrypointView {
        name,
        role,
        path: path.display().to_string(),
        required,
        status: if path.is_file() { "loaded" } else { "missing" },
        note,
    }
}

fn resolve_source_order(
    repo_root: &Path,
    layout: &StateLayout,
    locality_id: Option<&str>,
    profile_kernel_ready: bool,
    registry_ready: bool,
) -> Result<SourceOrderView> {
    let Some(locality_id) = locality_id else {
        return Ok(SourceOrderView {
            status: "blocked",
            mode: "blocked",
            manifest_path: None,
            manifest_status: "unknown",
            entries: Vec::new(),
            active_sources: Vec::new(),
            note:
                "Effective source order is unavailable until this workspace is linked to a project ID."
                    .to_owned(),
        });
    };

    let manifest_path = layout.repo_manifest_path(locality_id)?;
    if !profile_kernel_ready || !registry_ready {
        return Ok(SourceOrderView {
            status: "blocked",
            mode: "blocked",
            manifest_path: Some(manifest_path.display().to_string()),
            manifest_status: "unknown",
            entries: Vec::new(),
            active_sources: Vec::new(),
            note: "Effective source order is unavailable until the profile kernel and project registry are bootstrapped."
                .to_owned(),
        });
    }

    let resolved = project_truth::resolve_manifest(repo_root, layout, locality_id)?;
    let note = match resolved.source_order {
        "manifest" => "Repo manifest order is active for session startup.",
        _ => "Default project-truth candidate order is active for session startup.",
    };

    Ok(SourceOrderView {
        status: "resolved",
        mode: resolved.source_order,
        manifest_path: Some(resolved.manifest_path.display().to_string()),
        manifest_status: resolved.manifest_status,
        entries: resolved
            .entries
            .into_iter()
            .map(|value| value.display().to_string())
            .collect(),
        active_sources: resolved
            .project_truth_paths
            .into_iter()
            .map(|value| value.display().to_string())
            .collect(),
        note: note.to_owned(),
    })
}

fn build_session_sequence(
    profile: &ProfileName,
    explicit_profile: Option<&str>,
    needs_scaffold: bool,
    needs_init: bool,
    needs_link: bool,
    locality_id: Option<&str>,
) -> Vec<SessionStepView> {
    let mut sequence = Vec::new();
    if needs_scaffold {
        sequence.push(SessionStepView {
            command: "ccd scaffold --path .".to_owned(),
            status: "required",
            reason: "Seed the canonical project-truth entrypoints before starting a session."
                .to_owned(),
        });
    }
    if needs_init {
        sequence.push(SessionStepView {
            command: with_profile_flag("ccd attach --path .", profile, explicit_profile),
            status: "required",
            reason: format!(
                "Bootstrap profile `{profile}` and link this workspace before running session commands."
            ),
        });
    } else if needs_link {
        sequence.push(SessionStepView {
            command: with_profile_flag(
                &format!(
                    "ccd link --path . --project-id {}",
                    locality_id.expect("project id is available when link is required")
                ),
                profile,
                explicit_profile,
            ),
            status: "required",
            reason: "Repair the project registry link before running session commands.".to_owned(),
        });
    }

    let standard_status = if sequence.is_empty() {
        "ready"
    } else {
        "pending"
    };
    let standard_reason = if sequence.is_empty() {
        "Ready to run now."
    } else {
        "Run after the required prep step(s) succeed."
    };

    sequence.push(SessionStepView {
        command: doctor_command(profile, explicit_profile),
        status: standard_status,
        reason: format!("{standard_reason} Verify project state before loading session context."),
    });
    sequence.push(SessionStepView {
        command: with_profile_flag("ccd start --activate --path .", profile, explicit_profile),
        status: standard_status,
        reason: format!(
            "{standard_reason} Load the effective session context and record workspace-local session telemetry in one call."
        ),
    });

    sequence
}

fn doctor_command(profile: &ProfileName, explicit_profile: Option<&str>) -> String {
    if explicit_profile.is_none() && profile.as_str() == DEFAULT_PROFILE {
        "ccd doctor --path .".to_owned()
    } else {
        format!("CCD_PROFILE={} ccd doctor --path .", profile.as_str())
    }
}

fn with_profile_flag(
    command: &str,
    profile: &ProfileName,
    explicit_profile: Option<&str>,
) -> String {
    if explicit_profile.is_none() && profile.as_str() == DEFAULT_PROFILE {
        command.to_owned()
    } else {
        format!("{command} --profile {}", profile.as_str())
    }
}
