use std::path::{Path, PathBuf};
use std::process::{Command, ExitCode};
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};

use anyhow::{bail, Context, Result};
use serde::Serialize;
use sha2::{Digest, Sha256};
use tracing::debug;

use crate::memory::entries::StructuredMemoryEntry;
use crate::memory::file::{append_block_to_contents, render_entry_block, write_memory_file};
use crate::output::CommandReport;
use crate::paths::git as git_paths;
use crate::paths::state::StateLayout;
use crate::profile;
use crate::repo::marker as repo_marker;
use crate::state::{compiled as compiled_state, projection_metadata};
use crate::state::{
    runtime::{self as runtime_state, RuntimeTextSurfaceStatus},
    session as session_state,
};

const ULID_ALPHABET: &[u8; 32] = b"0123456789ABCDEFGHJKMNPQRSTVWXYZ";
static ULID_COUNTER: AtomicU64 = AtomicU64::new(0);

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum RememberScope {
    Profile,
    Repo,
    Branch,
    Clone,
}

impl RememberScope {
    fn label(self) -> &'static str {
        match self {
            Self::Profile => "profile",
            Self::Repo => "project",
            Self::Branch => "work_stream",
            Self::Clone => "workspace",
        }
    }
}

#[derive(Serialize)]
pub struct RememberReport {
    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>,
    scope: &'static str,
    mode: &'static str,
    target: MemoryTargetView,
    generated_entry: StructuredMemoryEntry,
    entry_block: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    write_result: Option<WriteResultView>,
    warnings: Vec<String>,
}

#[derive(Serialize)]
struct MemoryTargetView {
    scope: &'static str,
    path: String,
    status: &'static str,
}

#[derive(Serialize)]
struct WriteResultView {
    file_action: &'static str,
    path: String,
    compiled_state_action: &'static str,
}

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

    fn render_text(&self) {
        let scope_label = self.scope.replace('_', " ");
        match &self.write_result {
            Some(write_result) => {
                println!(
                    "Captured memory entry {} in {} memory.",
                    self.generated_entry.id, scope_label
                );
                println!("Target: {} ({})", self.target.path, self.target.status);
                println!(
                    "Compiled state: {}",
                    write_result.compiled_state_action.replace('_', " ")
                );
            }
            None => {
                println!("Prepared memory capture preview for {scope_label} memory.");
                println!("Target: {} ({})", self.target.path, self.target.status);
            }
        }
        println!("Entry type: {}", self.generated_entry.entry_type);
        println!("Origin: {}", self.generated_entry.origin);
        println!(
            "Last touched session: {}",
            self.generated_entry.last_touched_session
        );

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

        println!();
        println!("Generated block:");
        println!("{}", self.entry_block);
    }
}

#[allow(clippy::too_many_arguments)]
pub fn run(
    path: &Path,
    explicit_profile: Option<&str>,
    requested_scope: Option<RememberScope>,
    entry_type: &str,
    origin: &str,
    source_ref: Option<&str>,
    dry_run: bool,
    content: &str,
) -> Result<RememberReport> {
    if content.trim().is_empty() {
        bail!("memory content cannot be empty");
    }
    if let Some(source_ref) = source_ref {
        if source_ref.trim().is_empty() {
            bail!("--source-ref cannot be empty");
        }
    }

    let repo_root = resolve_project_root(path)?;
    let profile = profile::resolve(explicit_profile)?;
    let layout = StateLayout::resolve(&repo_root, profile.clone())?;
    ensure_profile_exists(&layout, &repo_root)?;

    let marker = repo_marker::load(&repo_root)?;
    let scope = resolve_scope(requested_scope, marker.as_ref());

    let target = match scope {
        RememberScope::Profile => load_profile_target(&layout)?,
        RememberScope::Repo => load_repo_target(&layout, &repo_root, marker.as_ref())?,
        RememberScope::Branch => load_branch_target(&layout, &repo_root, marker.as_ref())?,
        RememberScope::Clone => load_clone_target(&layout, &repo_root, marker.as_ref())?,
    };

    let generated_entry = StructuredMemoryEntry {
        id: generate_memory_id()?,
        entry_type: entry_type.to_owned(),
        state: "active".to_owned(),
        created_at: current_utc_rfc3339()?,
        last_touched_session: current_session_start_count(&layout)?,
        origin: origin.to_owned(),
        superseded_at: None,
        decay_class: None,
        expires_at: None,
        tags: Vec::new(),
        source_ref: source_ref.map(ToOwned::to_owned),
        supersedes: Vec::new(),
        content: content.to_owned(),
    };
    let entry_block = render_entry_block(&generated_entry);

    let write_result = if dry_run {
        None
    } else {
        debug!(scope = %scope.label(), path = %target.path.display(), "writing memory entry");
        let next_contents = append_block_to_contents(&target.contents, &entry_block);
        write_memory_file(&target.path, &next_contents)?;

        let compiled_state_action = if let Some(marker) = marker.as_ref() {
            let compiled =
                compiled_state::refresh_after_write(&repo_root, &layout, &marker.locality_id)?;
            if let Err(error) = projection_metadata::record_for_compiled_store(&layout, &compiled) {
                projection_metadata::warn_record_error(&layout, &error);
            }
            "refreshed"
        } else {
            "not_applicable"
        };

        Some(WriteResultView {
            file_action: "append_block",
            path: target.path.display().to_string(),
            compiled_state_action,
        })
    };

    Ok(RememberReport {
        command: "remember",
        ok: true,
        path: repo_root.display().to_string(),
        profile: profile.to_string(),
        project_id: marker.as_ref().map(|marker| marker.locality_id.clone()),
        locality_id: marker.as_ref().map(|marker| marker.locality_id.clone()),
        scope: scope.label(),
        mode: if dry_run { "dry_run" } else { "write" },
        target: MemoryTargetView {
            scope: scope.label(),
            path: target.path.display().to_string(),
            status: target.status,
        },
        generated_entry,
        entry_block,
        write_result,
        warnings: Vec::new(),
    })
}

struct LoadedMemoryTarget {
    path: PathBuf,
    status: &'static str,
    contents: String,
}

fn authored_memory_target(
    path: PathBuf,
    source_status: RuntimeTextSurfaceStatus,
    contents: String,
) -> LoadedMemoryTarget {
    LoadedMemoryTarget {
        status: match source_status {
            RuntimeTextSurfaceStatus::Missing | RuntimeTextSurfaceStatus::LoadedNative => "missing",
            RuntimeTextSurfaceStatus::Empty => "empty",
            RuntimeTextSurfaceStatus::Loaded => "loaded",
        },
        path,
        contents,
    }
}

fn load_profile_target(layout: &StateLayout) -> Result<LoadedMemoryTarget> {
    let surface = runtime_state::load_profile_memory_surface(layout)?;
    Ok(authored_memory_target(
        layout.profile_memory_path(),
        surface.source.status,
        surface.source.content,
    ))
}

fn load_repo_target(
    layout: &StateLayout,
    repo_root: &Path,
    marker: Option<&repo_marker::RepoMarker>,
) -> Result<LoadedMemoryTarget> {
    let marker = marker.ok_or_else(|| {
        anyhow::anyhow!(
            "repo is not linked: {} is missing; run `ccd attach --path {}` or `ccd link --path {}` first",
            repo_root.join(repo_marker::MARKER_FILE).display(),
            repo_root.display(),
            repo_root.display()
        )
    })?;
    ensure_repo_registry_exists(layout, repo_root, &marker.locality_id)?;
    ensure_repo_overlay_exists(layout, repo_root, &marker.locality_id)?;
    let surface = runtime_state::load_locality_memory_surface(layout, &marker.locality_id)?;
    Ok(authored_memory_target(
        layout.locality_memory_path(&marker.locality_id)?,
        surface.source.status,
        surface.source.content,
    ))
}

fn load_clone_target(
    layout: &StateLayout,
    repo_root: &Path,
    marker: Option<&repo_marker::RepoMarker>,
) -> Result<LoadedMemoryTarget> {
    let marker = marker.ok_or_else(|| {
        anyhow::anyhow!(
            "repo is not linked: {} is missing; run `ccd attach --path {}` or `ccd link --path {}` first",
            repo_root.join(repo_marker::MARKER_FILE).display(),
            repo_root.display(),
            repo_root.display()
        )
    })?;
    ensure_repo_registry_exists(layout, repo_root, &marker.locality_id)?;
    let surface = runtime_state::load_clone_memory_surface(layout)?;
    Ok(LoadedMemoryTarget {
        path: surface.source.path,
        status: surface.source.status.as_str(),
        contents: surface.source.content,
    })
}

fn load_branch_target(
    layout: &StateLayout,
    repo_root: &Path,
    marker: Option<&repo_marker::RepoMarker>,
) -> Result<LoadedMemoryTarget> {
    let marker = marker.ok_or_else(|| {
        anyhow::anyhow!(
            "repo is not linked: {} is missing; run `ccd attach --path {}` or `ccd link --path {}` first",
            repo_root.join(repo_marker::MARKER_FILE).display(),
            repo_root.display(),
            repo_root.display()
        )
    })?;
    ensure_repo_registry_exists(layout, repo_root, &marker.locality_id)?;
    if !layout.resolved_substrate().is_git() {
        bail!(
            "branch memory is unavailable for directory-substrate workspaces because no active work stream exists; use profile, repo, or clone scope instead"
        );
    }
    ensure_repo_overlay_exists(layout, repo_root, &marker.locality_id)?;
    if runtime_state::resolve_active_branch_memory_path(repo_root, layout, &marker.locality_id)?
        .is_none()
    {
        bail!(
            "work-stream memory is only available on active non-trunk named branches; switch to a feature branch before using `ccd remember --scope work-stream`"
        );
    }
    let surface =
        runtime_state::load_branch_memory_surface(repo_root, layout, &marker.locality_id)?;
    Ok(LoadedMemoryTarget {
        path: surface.source.path,
        status: surface.source.status.as_str(),
        contents: surface.source.content,
    })
}

fn resolve_scope(
    requested_scope: Option<RememberScope>,
    marker: Option<&repo_marker::RepoMarker>,
) -> RememberScope {
    requested_scope.unwrap_or_else(|| {
        if marker.is_some() {
            RememberScope::Repo
        } else {
            RememberScope::Profile
        }
    })
}

fn resolve_project_root(path: &Path) -> Result<PathBuf> {
    if git_paths::is_git_work_tree(path) {
        return resolve_git_repo_root(path);
    }

    if let Some(root) = resolve_directory_project_root(path)? {
        return Ok(root);
    }

    bail!(
        "`ccd remember` requires --path to be inside a git checkout or an attached directory workspace; run `ccd attach --path {}` first for directory projects",
        path.display()
    );
}

fn resolve_directory_project_root(path: &Path) -> Result<Option<PathBuf>> {
    for ancestor in path.ancestors() {
        let Some(marker) = repo_marker::load(ancestor)? else {
            continue;
        };
        if marker.substrate() == repo_marker::MarkerSubstrate::Directory {
            return Ok(Some(ancestor.to_path_buf()));
        }
    }

    Ok(None)
}

fn resolve_git_repo_root(path: &Path) -> Result<PathBuf> {
    let output = Command::new("git")
        .args(["rev-parse", "--show-toplevel"])
        .current_dir(path)
        .output()
        .with_context(|| {
            format!(
                "failed to run `git rev-parse --show-toplevel` in {}",
                path.display()
            )
        })?;
    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        let detail = stderr.trim();
        if detail.is_empty() {
            bail!(
                "`git rev-parse --show-toplevel` failed in {}",
                path.display()
            );
        }
        bail!(
            "`git rev-parse --show-toplevel` failed in {}: {detail}",
            path.display()
        );
    }

    let stdout = String::from_utf8(output.stdout)
        .context("git returned a non-utf8 path while resolving repo root")?;
    let repo_root = PathBuf::from(stdout.trim());
    repo_root
        .canonicalize()
        .with_context(|| format!("failed to resolve repo root {}", repo_root.display()))
}

fn ensure_profile_exists(layout: &StateLayout, repo_root: &Path) -> Result<()> {
    let profile_root = layout.profile_root();
    if profile_root.is_dir() {
        return Ok(());
    }

    bail!(
        "profile `{}` does not exist at {}; bootstrap it with `ccd attach --path {}` before using `ccd remember`",
        layout.profile(),
        profile_root.display(),
        repo_root.display()
    )
}

fn ensure_repo_registry_exists(
    layout: &StateLayout,
    repo_root: &Path,
    locality_id: &str,
) -> Result<()> {
    let metadata_path = layout.repo_metadata_path(locality_id)?;
    if metadata_path.is_file() {
        return Ok(());
    }

    bail!(
        "repo `{}` is linked from {} but the registry entry is missing at {}; run `ccd attach --path {}` or `ccd link --path {}` to repair the overlay before using `ccd remember`",
        locality_id,
        repo_root.display(),
        metadata_path.display(),
        repo_root.display(),
        repo_root.display()
    )
}

fn ensure_repo_overlay_exists(
    layout: &StateLayout,
    repo_root: &Path,
    locality_id: &str,
) -> Result<()> {
    let overlay_root = layout.repo_overlay_root(locality_id)?;
    if overlay_root.is_dir() {
        return Ok(());
    }

    bail!(
        "repo overlay for `{}` is missing at {}; run `ccd attach --path {}` or `ccd link --path {}` to restore it before using `ccd remember`",
        locality_id,
        overlay_root.display(),
        repo_root.display(),
        repo_root.display()
    )
}

fn current_session_start_count(layout: &StateLayout) -> Result<u64> {
    Ok(session_state::load_for_layout(layout)?
        .map(|state| u64::from(state.start_count))
        .unwrap_or(0))
}

fn generate_memory_id() -> Result<String> {
    let now = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .context("system clock is before UNIX_EPOCH")?;
    let timestamp_ms = now.as_millis();
    if timestamp_ms > 0xFFFF_FFFF_FFFF {
        bail!("current time exceeds ULID timestamp range");
    }

    let mut bytes = [0u8; 16];
    bytes[..6].copy_from_slice(&(timestamp_ms as u64).to_be_bytes()[2..]);

    let counter = ULID_COUNTER.fetch_add(1, Ordering::Relaxed);
    let mut entropy = Sha256::new();
    entropy.update(now.as_nanos().to_be_bytes());
    entropy.update(counter.to_be_bytes());
    entropy.update(std::process::id().to_be_bytes());
    let digest = entropy.finalize();
    bytes[6..].copy_from_slice(&digest[..10]);

    Ok(format!("mem_{}", encode_ulid(bytes)))
}

fn encode_ulid(bytes: [u8; 16]) -> String {
    let mut encoded = String::with_capacity(26);
    let mut buffer = 0u32;
    let mut bits = 2u8;

    for byte in bytes {
        buffer = (buffer << 8) | u32::from(byte);
        bits += 8;
        while bits >= 5 {
            let shift = bits - 5;
            let index = ((buffer >> shift) & 0x1f) as usize;
            encoded.push(ULID_ALPHABET[index] as char);
            buffer &= (1u32 << shift) - 1;
            bits -= 5;
        }
    }

    encoded
}

fn current_utc_rfc3339() -> Result<String> {
    let now = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .context("system clock is before UNIX_EPOCH")?;
    let seconds = now.as_secs() as i64;
    let days = seconds.div_euclid(86_400);
    let seconds_of_day = seconds.rem_euclid(86_400);
    let (year, month, day) = civil_from_days(days);
    let hour = seconds_of_day / 3_600;
    let minute = (seconds_of_day % 3_600) / 60;
    let second = seconds_of_day % 60;

    Ok(format!(
        "{year:04}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}Z"
    ))
}

fn civil_from_days(days_since_epoch: i64) -> (i64, i64, i64) {
    let z = days_since_epoch + 719_468;
    let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
    let day_of_era = z - era * 146_097;
    let year_of_era =
        (day_of_era - day_of_era / 1_460 + day_of_era / 36_524 - day_of_era / 146_096) / 365;
    let mut year = year_of_era + era * 400;
    let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100);
    let month_prime = (5 * day_of_year + 2) / 153;
    let day = day_of_year - (153 * month_prime + 2) / 5 + 1;
    let month = month_prime + if month_prime < 10 { 3 } else { -9 };
    if month <= 2 {
        year += 1;
    }

    (year, month, day)
}

#[cfg(test)]
mod tests {
    use super::{civil_from_days, encode_ulid};

    #[test]
    fn civil_from_days_handles_epoch_boundary() {
        assert_eq!(civil_from_days(0), (1970, 1, 1));
        assert_eq!(civil_from_days(1), (1970, 1, 2));
    }

    #[test]
    fn encode_ulid_preserves_expected_width() {
        let encoded = encode_ulid([0u8; 16]);
        assert_eq!(encoded, "00000000000000000000000000");
    }
}
