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

use anyhow::{bail, Context, Result};
use serde::Serialize;
use serde_json::Value as JsonValue;

use super::{attach, doctor, start};
use crate::output::CommandReport;
use crate::paths::{git as git_paths, state::StateLayout, substrate::SubstrateKind};
use crate::profile::{self, ProfileName};
use crate::repo::marker as repo_marker;
use crate::repo::registry as repo_registry;
use crate::state::session as session_state;

#[derive(Serialize)]
pub struct SessionOpenReport {
    command: &'static str,
    ok: bool,
    path: String,
    profile: String,
    worktree: WorktreeView,
    bootstrap: BootstrapView,
    doctor: DoctorView,
    start: StartView,
    session_state: SessionStateView,
    warnings: Vec<String>,
}

#[derive(Serialize)]
struct WorktreeView {
    status: &'static str,
    path: String,
    branch: Option<String>,
    from: Option<String>,
}

#[derive(Serialize)]
struct BootstrapView {
    status: &'static str,
    action: Option<String>,
    resolution: Option<String>,
}

#[derive(Serialize)]
struct DoctorView {
    status: &'static str,
    failures: u64,
    warnings: u64,
}

#[derive(Serialize)]
struct StartView {
    status: &'static str,
    project_id: Option<String>,
    locality_id: Option<String>,
    handoff_path: Option<String>,
}

#[derive(Serialize)]
struct SessionStateView {
    status: &'static str,
    path: Option<String>,
    start_count: Option<u64>,
}

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

    fn render_text(&self) {
        let status = if self.ok { "opened" } else { "blocked" };
        println!(
            "Session open is {status} for profile `{}` at {}.",
            self.profile, self.path
        );

        match self.worktree.status {
            "created" => {
                let branch = self.worktree.branch.as_deref().unwrap_or("unknown");
                if let Some(from) = &self.worktree.from {
                    println!(
                        "Worktree: created {} on branch `{branch}` from `{from}`.",
                        self.worktree.path
                    );
                } else {
                    println!(
                        "Worktree: created {} on branch `{branch}`.",
                        self.worktree.path
                    );
                }
            }
            _ => println!(
                "Worktree: using existing checkout at {}.",
                self.worktree.path
            ),
        }

        match self.bootstrap.status {
            "initialized" => {
                let action = self.bootstrap.action.as_deref().unwrap_or("updated");
                let resolution = self.bootstrap.resolution.as_deref().unwrap_or("resolved");
                println!("Bootstrap: {action} profile state ({resolution}).");
            }
            _ => println!("Bootstrap: existing profile state was reused."),
        }

        println!(
            "Doctor summary: {} failure(s), {} warning(s).",
            self.doctor.failures, self.doctor.warnings
        );

        if !self.ok {
            println!("Session start was skipped because doctor reported failures.");
            return;
        }

        if let Some(project_id) = self
            .start
            .project_id
            .as_ref()
            .or(self.start.locality_id.as_ref())
        {
            println!("Project ID: {project_id}");
        }
        if let Some(handoff_path) = &self.start.handoff_path {
            println!("Handoff: {handoff_path}");
        }
        if let Some(path) = &self.session_state.path {
            let start_count = self.session_state.start_count.unwrap_or(0);
            println!("Session state: {path} (start_count={start_count})");
        }

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

pub fn run(
    repo_root: &Path,
    explicit_profile: Option<&str>,
    worktree: Option<&Path>,
    branch: Option<&str>,
    from: Option<&str>,
    pod: Option<&str>,
) -> Result<SessionOpenReport> {
    if pod.is_some() {
        bail!(
            "`--pod` is retired under RFC 0006 M4(d); `ccd session open` no longer \
             wires coordination scopes. Set `[dispatch].coordination_scope` in the \
             repo overlay config (`.ccd/profiles/<profile>/repos/<project_id>/config.toml`) \
             for both source and target profiles before opening the session."
        );
    }
    validate_args(worktree, branch, from)?;
    let profile = profile::resolve(explicit_profile)?;
    let layout = StateLayout::resolve_for_attach(repo_root, profile.clone())?;
    if layout.resolved_substrate().kind() != SubstrateKind::Git {
        bail!(
            "`ccd session open` is only supported for Git-backed projects; directory substrate workspaces have no non-Git session isolation yet"
        );
    }

    let mut target_path = repo_root.to_path_buf();
    let worktree_view = if let Some(worktree) = worktree {
        let target = resolve_worktree_path(repo_root, worktree)?;
        let branch = branch.expect("branch is validated");
        git_paths::worktree_add(repo_root, &target, branch, from)?;
        target_path = target.clone();
        WorktreeView {
            status: "created",
            path: target.display().to_string(),
            branch: Some(branch.to_owned()),
            from: from.map(str::to_owned),
        }
    } else {
        WorktreeView {
            status: "existing",
            path: target_path.display().to_string(),
            branch: None,
            from: None,
        }
    };

    let bootstrap = if needs_bootstrap(&target_path, &profile)? {
        let report = attach::run(&target_path, explicit_profile, None, None)?;
        let json = serde_json::to_value(&report)?;
        BootstrapView {
            status: "initialized",
            action: string_at(&json, &["action"]),
            resolution: string_at(&json, &["resolution"]),
        }
    } else {
        BootstrapView {
            status: "existing",
            action: None,
            resolution: None,
        }
    };

    let doctor_report = doctor::run(
        &target_path,
        explicit_profile,
        doctor::RunOptions {
            include_repo_native_checks: true,
        },
    )?;
    let doctor_json = serde_json::to_value(&doctor_report)?;
    let doctor_failures = u64_at(&doctor_json, &["failures"]).unwrap_or(0);
    let doctor_warnings = u64_at(&doctor_json, &["warnings"]).unwrap_or(0);

    if doctor_failures > 0 {
        return Ok(SessionOpenReport {
            command: "session-open",
            ok: false,
            path: target_path.display().to_string(),
            profile: profile.to_string(),
            worktree: worktree_view,
            bootstrap,
            doctor: DoctorView {
                status: "failed",
                failures: doctor_failures,
                warnings: doctor_warnings,
            },
            start: StartView {
                status: "skipped",
                project_id: None,
                locality_id: None,
                handoff_path: None,
            },
            session_state: SessionStateView {
                status: "skipped",
                path: None,
                start_count: None,
            },
            warnings: Vec::new(),
        });
    }

    let start_report = start::run(&target_path, explicit_profile, false)?;
    let start_json = serde_json::to_value(&start_report)?;
    let locality_id = repo_marker::load(&target_path)
        .ok()
        .flatten()
        .map(|m| m.locality_id);
    let session_report = session_state::start(
        &target_path,
        explicit_profile,
        locality_id.as_deref(),
        session_state::SessionStartOptions::interactive(None),
    )?;
    let session_json = serde_json::to_value(&session_report)?;

    Ok(SessionOpenReport {
        command: "session-open",
        ok: true,
        path: target_path.display().to_string(),
        profile: profile.to_string(),
        worktree: worktree_view,
        bootstrap,
        doctor: DoctorView {
            status: if doctor_warnings > 0 {
                "warning"
            } else {
                "passed"
            },
            failures: doctor_failures,
            warnings: doctor_warnings,
        },
        start: StartView {
            status: "started",
            project_id: string_at(&start_json, &["project_id"]),
            locality_id: string_at(&start_json, &["locality_id"]),
            handoff_path: string_at(&start_json, &["handoff", "path"]),
        },
        session_state: SessionStateView {
            status: "recorded",
            path: string_at(&session_json, &["path"]),
            start_count: u64_at(&session_json, &["start_count"]),
        },
        warnings: string_array_at(&start_json, &["warnings"]),
    })
}

fn validate_args(worktree: Option<&Path>, branch: Option<&str>, from: Option<&str>) -> Result<()> {
    if worktree.is_none() {
        if branch.is_some() {
            bail!("`--branch` requires `--worktree`");
        }
        if from.is_some() {
            bail!("`--from` requires `--worktree`");
        }
        return Ok(());
    }

    let Some(branch) = branch else {
        bail!("`--branch` is required when `--worktree` is set");
    };
    if branch.trim().is_empty() {
        bail!("`--branch` cannot be empty");
    }
    if let Some(from) = from {
        if from.trim().is_empty() {
            bail!("`--from` cannot be empty");
        }
    }

    Ok(())
}

fn needs_bootstrap(repo_root: &Path, profile: &ProfileName) -> Result<bool> {
    let layout = StateLayout::resolve(repo_root, profile.clone())?;
    if !layout.profile_root().is_dir() {
        return Ok(true);
    }

    let Some(marker) = repo_marker::load(repo_root)? else {
        return Ok(true);
    };
    let registry_path = layout.repo_metadata_path(&marker.locality_id)?;
    if repo_registry::load(&registry_path)?.is_none() {
        return Ok(true);
    }

    let overlay_path = layout.repo_overlay_root(&marker.locality_id)?;
    Ok(!overlay_path.is_dir())
}

fn resolve_worktree_path(repo_root: &Path, worktree: &Path) -> Result<PathBuf> {
    let raw = worktree
        .to_str()
        .ok_or_else(|| anyhow::anyhow!("unsafe --worktree value: paths must be valid UTF-8"))?;
    if raw.chars().any(char::is_control) {
        bail!("unsafe --worktree value: control characters are not allowed");
    }

    let anchored = if worktree.is_absolute() {
        worktree.to_path_buf()
    } else {
        repo_root.join(worktree)
    };

    let file_name = anchored
        .file_name()
        .and_then(|value| value.to_str())
        .filter(|value| !value.is_empty() && *value != "." && *value != "..")
        .ok_or_else(|| {
            anyhow::anyhow!("unsafe --worktree value: target directory name is required")
        })?;
    let parent = anchored.parent().unwrap_or(repo_root);
    let parent = parent
        .canonicalize()
        .with_context(|| format!("failed to resolve worktree parent `{}`", parent.display()))?;
    let target = parent.join(file_name);

    if target.exists() {
        bail!(
            "worktree path already exists: {}; choose a new path or remove it first",
            target.display()
        );
    }
    if target.starts_with(repo_root) {
        bail!(
            "worktree path must be outside the source checkout: {}",
            target.display()
        );
    }

    Ok(target)
}

fn string_at(value: &JsonValue, path: &[&str]) -> Option<String> {
    find_value(value, path)
        .and_then(JsonValue::as_str)
        .map(str::to_owned)
}

fn u64_at(value: &JsonValue, path: &[&str]) -> Option<u64> {
    find_value(value, path).and_then(JsonValue::as_u64)
}

fn string_array_at(value: &JsonValue, path: &[&str]) -> Vec<String> {
    find_value(value, path)
        .and_then(JsonValue::as_array)
        .map(|entries| {
            entries
                .iter()
                .filter_map(JsonValue::as_str)
                .map(str::to_owned)
                .collect()
        })
        .unwrap_or_default()
}

fn find_value<'a>(value: &'a JsonValue, path: &[&str]) -> Option<&'a JsonValue> {
    let mut current = value;
    for segment in path {
        current = current.get(*segment)?;
    }
    Some(current)
}
