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

use serde::{Deserialize, Serialize};

const STATUS_FILE: &str = "renewal-status.json";
const LIFELOOP_SCHEMA_VERSION: &str = "lifeloop.v0.2";

#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum LifeloopRenewalStatusLoadStatus {
    Missing,
    Loaded,
    Invalid,
}

#[derive(Clone, Debug, Serialize)]
pub(crate) struct LifeloopRenewalStatusView {
    pub(crate) path: String,
    pub(crate) status: LifeloopRenewalStatusLoadStatus,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub(crate) schema_version: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub(crate) state: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub(crate) client_id: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub(crate) adapter_id: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub(crate) updated_at_epoch_s: Option<u64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub(crate) pending_token_present: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub(crate) reset_path: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub(crate) thread_id: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub(crate) renewal_lease_id: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub(crate) prepared_at_epoch_s: Option<u64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub(crate) fulfilled_at_epoch_s: Option<u64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub(crate) reset_prepare_receipt_path: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub(crate) failure_class: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub(crate) retry_class: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub(crate) message: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub(crate) warning: Option<String>,
}

#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct RawLifeloopRenewalStatus {
    schema_version: String,
    state: String,
    client_id: String,
    adapter_id: String,
    updated_at_epoch_s: u64,
    pending_token_present: bool,
    reset_path: Option<String>,
    thread_id: Option<String>,
    renewal_lease_id: Option<String>,
    prepared_at_epoch_s: Option<u64>,
    fulfilled_at_epoch_s: Option<u64>,
    pending_path: Option<String>,
    reset_prepare_receipt_path: Option<String>,
    failure_class: Option<String>,
    retry_class: Option<String>,
    message: Option<String>,
}

impl LifeloopRenewalStatusView {
    pub(crate) fn warning_message(&self) -> Option<String> {
        match self.status {
            LifeloopRenewalStatusLoadStatus::Invalid => self.warning.clone(),
            LifeloopRenewalStatusLoadStatus::Missing => None,
            LifeloopRenewalStatusLoadStatus::Loaded => match self.state.as_deref() {
                Some("pending_continuation") => Some(
                    "Lifeloop reports a pending renewal continuation; restart through the Lifeloop renewal profile before continuing.".to_owned(),
                ),
                Some("failed") | Some("expired") => Some(format!(
                    "Lifeloop renewal automation reported {}: {}",
                    self.state.as_deref().unwrap_or("failed"),
                    self.message
                        .as_deref()
                        .unwrap_or("no diagnostic message was recorded")
                )),
                _ => None,
            },
        }
    }

    pub(crate) fn evidence_line(&self) -> Option<String> {
        match self.status {
            LifeloopRenewalStatusLoadStatus::Missing => None,
            LifeloopRenewalStatusLoadStatus::Invalid => Some(format!(
                "Lifeloop renewal status at {} is invalid: {}",
                self.path,
                self.warning.as_deref().unwrap_or("invalid status document")
            )),
            LifeloopRenewalStatusLoadStatus::Loaded => {
                let state = self.state.as_deref().unwrap_or("unknown");
                let adapter = self.adapter_id.as_deref().unwrap_or("unknown");
                let mut parts = vec![format!(
                    "Lifeloop renewal status `{state}` for adapter `{adapter}`"
                )];
                if let Some(thread_id) = self.thread_id.as_deref() {
                    parts.push(format!("thread `{thread_id}`"));
                }
                if let Some(failure_class) = self.failure_class.as_deref() {
                    parts.push(format!("failure_class `{failure_class}`"));
                }
                if let Some(retry_class) = self.retry_class.as_deref() {
                    parts.push(format!("retry_class `{retry_class}`"));
                }
                if let Some(message) = self.message.as_deref() {
                    parts.push(message.to_owned());
                }
                Some(format!("{}.", parts.join("; ")))
            }
        }
    }

    pub(crate) fn blocks_start_without_continuation(&self) -> bool {
        self.status == LifeloopRenewalStatusLoadStatus::Loaded
            && self.state.as_deref() == Some("pending_continuation")
    }

    pub(crate) fn is_failed_like(&self) -> bool {
        self.status == LifeloopRenewalStatusLoadStatus::Loaded
            && matches!(self.state.as_deref(), Some("failed" | "expired"))
    }
}

pub(crate) fn load_view(repo_root: &Path) -> LifeloopRenewalStatusView {
    let path = status_path(repo_root);
    let path_string = path.display().to_string();
    if !path.exists() {
        return LifeloopRenewalStatusView {
            path: path_string,
            status: LifeloopRenewalStatusLoadStatus::Missing,
            schema_version: None,
            state: None,
            client_id: None,
            adapter_id: None,
            updated_at_epoch_s: None,
            pending_token_present: None,
            reset_path: None,
            thread_id: None,
            renewal_lease_id: None,
            prepared_at_epoch_s: None,
            fulfilled_at_epoch_s: None,
            reset_prepare_receipt_path: None,
            failure_class: None,
            retry_class: None,
            message: None,
            warning: None,
        };
    }

    let raw = match fs::read_to_string(&path) {
        Ok(raw) => raw,
        Err(error) => {
            return invalid_view(path_string, format!("failed to read status: {error}"));
        }
    };
    let parsed: RawLifeloopRenewalStatus = match serde_json::from_str(&raw) {
        Ok(parsed) => parsed,
        Err(error) => {
            return invalid_view(path_string, format!("invalid JSON: {error}"));
        }
    };
    if parsed.schema_version != LIFELOOP_SCHEMA_VERSION {
        return invalid_view(
            path_string,
            format!(
                "schema_version mismatch: expected `{LIFELOOP_SCHEMA_VERSION}`, found `{}`",
                parsed.schema_version
            ),
        );
    }
    if !known_state(&parsed.state) {
        return invalid_view(
            path_string,
            format!("unknown Lifeloop renewal state `{}`", parsed.state),
        );
    }
    if parsed.client_id.trim().is_empty() || parsed.adapter_id.trim().is_empty() {
        return invalid_view(
            path_string,
            "client_id and adapter_id must be non-empty".to_owned(),
        );
    }
    if [
        &parsed.reset_path,
        &parsed.thread_id,
        &parsed.renewal_lease_id,
        &parsed.pending_path,
        &parsed.reset_prepare_receipt_path,
        &parsed.failure_class,
        &parsed.retry_class,
        &parsed.message,
    ]
    .iter()
    .any(|value| {
        value
            .as_deref()
            .is_some_and(|value| value.trim().is_empty())
    }) {
        return invalid_view(
            path_string,
            "optional string fields must be non-empty".to_owned(),
        );
    }
    if parsed.state == "pending_continuation" && !parsed.pending_token_present {
        return invalid_view(
            path_string,
            "pending_continuation requires pending_token_present=true".to_owned(),
        );
    }
    let failed_like = matches!(parsed.state.as_str(), "failed" | "expired");
    match (
        failed_like,
        parsed.failure_class.as_ref(),
        parsed.retry_class.as_ref(),
    ) {
        (true, Some(_), Some(_)) | (false, None, None) => {}
        (true, _, _) => {
            return invalid_view(
                path_string,
                "failed or expired renewal status requires failure_class and retry_class"
                    .to_owned(),
            );
        }
        (false, _, _) => {
            return invalid_view(
                path_string,
                "failure_class and retry_class are only valid for failed or expired renewal status"
                    .to_owned(),
            );
        }
    }

    LifeloopRenewalStatusView {
        path: path_string,
        status: LifeloopRenewalStatusLoadStatus::Loaded,
        schema_version: Some(parsed.schema_version),
        state: Some(parsed.state),
        client_id: Some(parsed.client_id),
        adapter_id: Some(parsed.adapter_id),
        updated_at_epoch_s: Some(parsed.updated_at_epoch_s),
        pending_token_present: Some(parsed.pending_token_present),
        reset_path: parsed.reset_path,
        thread_id: parsed.thread_id,
        renewal_lease_id: parsed.renewal_lease_id,
        prepared_at_epoch_s: parsed.prepared_at_epoch_s,
        fulfilled_at_epoch_s: parsed.fulfilled_at_epoch_s,
        reset_prepare_receipt_path: parsed.reset_prepare_receipt_path,
        failure_class: parsed.failure_class,
        retry_class: parsed.retry_class,
        message: parsed.message,
        warning: None,
    }
}

pub(crate) fn status_path(repo_root: &Path) -> PathBuf {
    default_state_dir(repo_root).join(STATUS_FILE)
}

fn invalid_view(path: String, warning: String) -> LifeloopRenewalStatusView {
    LifeloopRenewalStatusView {
        path,
        status: LifeloopRenewalStatusLoadStatus::Invalid,
        schema_version: None,
        state: None,
        client_id: None,
        adapter_id: None,
        updated_at_epoch_s: None,
        pending_token_present: None,
        reset_path: None,
        thread_id: None,
        renewal_lease_id: None,
        prepared_at_epoch_s: None,
        fulfilled_at_epoch_s: None,
        reset_prepare_receipt_path: None,
        failure_class: None,
        retry_class: None,
        message: None,
        warning: Some(warning),
    }
}

fn default_state_dir(path: &Path) -> PathBuf {
    for candidate in path.ancestors() {
        let git_dir = candidate.join(".git");
        if git_dir.is_dir() {
            return git_dir.join("lifeloop").join("renewal");
        }
    }
    path.join(".lifeloop").join("renewal")
}

fn known_state(value: &str) -> bool {
    matches!(
        value,
        "not_attempted"
            | "no_renewal"
            | "prepare_started"
            | "receipt_written"
            | "lease_created"
            | "pending_continuation"
            | "fulfilled"
            | "failed"
            | "expired"
    )
}
