use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};

use crate::{ModuleRef, ProcessRef};

#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct LashlangExecutionContext {
    pub(crate) module_ref: ModuleRef,
    pub(crate) entry: LashlangExecutionEntry,
}

impl LashlangExecutionContext {
    pub(crate) fn main(module_ref: ModuleRef) -> Self {
        Self {
            module_ref,
            entry: LashlangExecutionEntry::Main,
        }
    }

    pub(crate) fn process(
        module_ref: ModuleRef,
        process_ref: ProcessRef,
        process_name: impl Into<String>,
    ) -> Self {
        Self {
            module_ref,
            entry: LashlangExecutionEntry::Process {
                process_ref,
                process_name: process_name.into(),
            },
        }
    }

    pub(crate) fn builder(&self) -> LashlangExecutionSiteBuilder<'_> {
        LashlangExecutionSiteBuilder { context: self }
    }
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum LashlangExecutionEntry {
    Main,
    Process {
        process_ref: ProcessRef,
        process_name: String,
    },
}

impl LashlangExecutionEntry {
    fn stable_key(&self) -> String {
        match self {
            Self::Main => "main".to_string(),
            Self::Process { process_ref, .. } => process_ref_key(process_ref),
        }
    }
}

#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub(crate) struct LashlangAstPath(Vec<u32>);

impl LashlangAstPath {
    pub(crate) fn root() -> Self {
        Self(Vec::new())
    }

    pub(crate) fn child(&self, index: usize) -> Self {
        let mut path = self.0.clone();
        path.push(index as u32);
        Self(path)
    }

    fn stable_text(&self) -> String {
        if self.0.is_empty() {
            return "root".to_string();
        }
        self.0
            .iter()
            .map(u32::to_string)
            .collect::<Vec<_>>()
            .join(".")
    }
}

#[derive(Clone, Debug)]
pub(crate) struct LashlangExecutionSiteBuilder<'context> {
    context: &'context LashlangExecutionContext,
}

impl LashlangExecutionSiteBuilder<'_> {
    pub(crate) fn process_node_id(&self) -> String {
        self.node_id(&LashlangAstPath::root(), "process")
    }

    pub(crate) fn main_node_id(&self) -> String {
        self.node_id(&LashlangAstPath::root(), "main")
    }

    pub(crate) fn node_site(
        &self,
        path: &LashlangAstPath,
        kind: impl Into<String>,
        label: impl Into<String>,
    ) -> LashlangExecutionSite {
        let kind = kind.into();
        LashlangExecutionSite {
            node_id: self.node_id(path, &kind),
            node_kind: kind,
            label: label.into(),
            branch: None,
        }
    }

    pub(crate) fn branch_site(&self, path: &LashlangAstPath) -> LashlangExecutionSite {
        LashlangExecutionSite {
            node_id: self.node_id(path, "branch"),
            node_kind: "branch".to_string(),
            label: "if".to_string(),
            branch: Some(LashlangBranchSite {
                then_edge_id: self.branch_edge_id(path, ProcessBranchSelection::Then),
                else_edge_id: self.branch_edge_id(path, ProcessBranchSelection::Else),
            }),
        }
    }

    pub(crate) fn branch_arm_node_id(
        &self,
        path: &LashlangAstPath,
        selection: ProcessBranchSelection,
    ) -> String {
        let kind = match selection {
            ProcessBranchSelection::Then => "branch_arm_then",
            ProcessBranchSelection::Else => "branch_arm_else",
        };
        self.node_id(path, kind)
    }

    pub(crate) fn edge_id(
        &self,
        path: &LashlangAstPath,
        from: &str,
        to: &str,
        label: &str,
    ) -> String {
        stable_id(
            "edge",
            &[
                self.context.module_ref.to_string(),
                self.context.entry.stable_key(),
                path.stable_text(),
                from.to_string(),
                to.to_string(),
                label.to_string(),
            ],
        )
    }

    pub(crate) fn branch_edge_id(
        &self,
        path: &LashlangAstPath,
        selection: ProcessBranchSelection,
    ) -> String {
        let label = match selection {
            ProcessBranchSelection::Then => "then",
            ProcessBranchSelection::Else => "else",
        };
        stable_id(
            "edge",
            &[
                self.context.module_ref.to_string(),
                self.context.entry.stable_key(),
                path.stable_text(),
                "branch".to_string(),
                label.to_string(),
            ],
        )
    }

    fn node_id(&self, path: &LashlangAstPath, kind: impl AsRef<str>) -> String {
        stable_id(
            kind.as_ref(),
            &[
                self.context.module_ref.to_string(),
                self.context.entry.stable_key(),
                path.stable_text(),
                kind.as_ref().to_string(),
            ],
        )
    }
}

fn stable_id(prefix: &str, parts: &[String]) -> String {
    let mut hasher = Sha256::new();
    for part in parts {
        hasher.update(part.as_bytes());
        hasher.update([0]);
    }
    let hash = format!("{:x}", hasher.finalize());
    format!("{prefix}:{}", &hash[..24])
}

pub fn process_ref_key(process_ref: &ProcessRef) -> String {
    format!("{}:{}", process_ref.component, process_ref.pos)
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct LashlangExecutionSite {
    pub node_id: String,
    pub node_kind: String,
    pub label: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub branch: Option<LashlangBranchSite>,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct LashlangExecutionCallSite {
    pub site: LashlangExecutionSite,
    pub occurrence: u64,
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct LashlangBranchSite {
    pub then_edge_id: String,
    pub else_edge_id: String,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ProcessBranchSelection {
    Then,
    Else,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct LashlangExecutionChild {
    pub process_id: String,
    pub module_ref: ModuleRef,
    pub process_ref: ProcessRef,
    pub process_name: String,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub enum LashlangExecutionObservation {
    NodeStarted {
        site: LashlangExecutionSite,
        occurrence: u64,
    },
    NodeCompleted {
        site: LashlangExecutionSite,
        occurrence: u64,
    },
    NodeFailed {
        site: LashlangExecutionSite,
        occurrence: u64,
        error: String,
    },
    BranchSelected {
        site: LashlangExecutionSite,
        occurrence: u64,
        edge_id: String,
        selected: ProcessBranchSelection,
    },
    ChildStarted {
        site: LashlangExecutionSite,
        occurrence: u64,
        child: LashlangExecutionChild,
    },
}
