// Copyright 2026 California Institute of Technology
//
// You should have received a copy of the licensing terms for this
// software included in the file “LICENSE” located in the top-level
// directory of this package. If you did not, you can view a copy at
// http://dcc.ligo.org/M1500244/LICENSE.txt

//! The background signal-acquisition thread.  It owns the CA context and all
//! channels; CA's callback threads deliver updates straight into the shared
//! store (preemptive callbacks), so this thread only blocks on its command
//! channel waiting for connect events and shutdown.

use crate::decode;
use crate::epics::{self, Context};
use crate::signal::{ChannelEntry, SignalInfo, SignalValue};
use epics_rs_sys as sys;
use std::collections::HashMap;
use std::sync::mpsc::{self, Receiver, Sender};
use std::sync::{Arc, Mutex};
use std::thread::JoinHandle;
use std::time::SystemTime;

/// The store shared with the display: latest value plus cached backend
/// description per channel name.
pub type SignalStore = Arc<Mutex<HashMap<String, ChannelEntry>>>;

enum Msg {
    Shutdown,
    /// A channel (by index) connected; subscribe it if we haven't yet.
    Connected(usize),
}

/// State shared between a channel's callbacks (which run on CA threads).
struct ChanState {
    name: String,
    store: SignalStore,
    labels: Mutex<Vec<String>>,
}

impl ChanState {
    /// Update the channel's value, leaving any cached description in place.
    fn store_value(&self, value: SignalValue) {
        if let Ok(mut map) = self.store.lock() {
            map.entry(self.name.clone())
                .or_insert_with(ChannelEntry::disconnected)
                .value = value;
        }
    }

    fn store_disconnected(&self) {
        self.store_value(SignalValue {
            data: None,
            time: SystemTime::now(),
        });
    }
}

pub struct Backend {
    tx: Sender<Msg>,
    join: JoinHandle<()>,
}

impl Backend {
    /// Spawn the acquisition thread.  The store is pre-filled with a
    /// disconnected entry for every channel before this returns.
    pub fn spawn(channels: Vec<String>, store: SignalStore) -> Self {
        {
            let mut map = store.lock().expect("store lock poisoned");
            for name in &channels {
                map.insert(name.clone(), ChannelEntry::disconnected());
            }
        }
        let (tx, rx) = mpsc::channel();
        let thread_tx = tx.clone();
        let join = std::thread::Builder::new()
            .name("epics-capture".to_string())
            .spawn(move || run(channels, store, thread_tx, rx))
            .expect("unable to spawn capture thread");
        Self { tx, join }
    }

    /// Stop the acquisition thread and wait for it to tear down CA.
    pub fn shutdown(self) {
        let _ = self.tx.send(Msg::Shutdown);
        let _ = self.join.join();
    }
}

fn run(names: Vec<String>, store: SignalStore, tx: Sender<Msg>, rx: Receiver<Msg>) {
    let context = match Context::create() {
        Ok(context) => context,
        Err(e) => {
            eprintln!("epics: unable to create context: {e}");
            return;
        }
    };

    let states: Vec<Arc<ChanState>> = names
        .iter()
        .map(|name| {
            Arc::new(ChanState {
                name: name.clone(),
                store: store.clone(),
                labels: Mutex::new(Vec::new()),
            })
        })
        .collect();

    // Channels must not outlive the context: they are dropped (reverse
    // declaration order) before `context` at the end of this function.
    let mut channels: Vec<Option<epics::Channel>> = Vec::with_capacity(names.len());
    for (index, name) in names.iter().enumerate() {
        let state = states[index].clone();
        let conn_tx = tx.clone();
        let connection_cb = Box::new(move |args: epics::connection_handler_args| {
            if epics::connection_up(&args) {
                let _ = conn_tx.send(Msg::Connected(index));
            } else {
                // No stale values: blank the entry immediately.
                state.store_disconnected();
            }
        });
        match context.create_channel(name, Some(connection_cb)) {
            Ok(channel) => channels.push(Some(channel)),
            Err(e) => {
                eprintln!("epics: unable to create channel {name}: {e}");
                channels.push(None);
            }
        }
    }
    let _ = context.flush_io();

    let mut subscribed = vec![false; names.len()];
    while let Ok(msg) = rx.recv() {
        match msg {
            Msg::Shutdown => break,
            Msg::Connected(index) => {
                let Some(channel) = channels[index].as_mut() else {
                    continue;
                };
                // Refresh the cached description on every connect (not just
                // the first): the serving host can change on a reconnect.
                if let Ok(mut map) = store.lock() {
                    map.entry(names[index].clone())
                        .or_insert_with(ChannelEntry::disconnected)
                        .info = Some(channel_info(channel));
                }
                if subscribed[index] {
                    // CA re-establishes existing subscriptions on reconnect.
                    continue;
                }
                match subscribe(channel, &states[index]) {
                    Ok(()) => subscribed[index] = true,
                    Err(e) => {
                        eprintln!("epics: unable to subscribe {}: {e}", states[index].name);
                    }
                }
                let _ = context.flush_io();
            }
        }
    }
}

/// The pvinfo-style description of a connected channel, from CA's local
/// accessors (no network round-trip).  Connection state is not included;
/// the UI derives it from the live value.
fn channel_info(channel: &epics::Channel) -> SignalInfo {
    let mut pairs = vec![("host".to_string(), channel.host_name())];
    let access = match (channel.read_access(), channel.write_access()) {
        (true, true) => "read, write",
        (true, false) => "read",
        (false, true) => "write",
        (false, false) => "none",
    };
    pairs.push(("access".to_string(), access.to_string()));
    if let Some(field_type) = channel.field_type() {
        pairs.push((
            "native type".to_string(),
            decode::field_type_name(field_type),
        ));
        if let Some(dbr_type) = decode::time_dbr_for_field(field_type) {
            pairs.push((
                "request type".to_string(),
                decode::request_type_name(dbr_type),
            ));
        }
    }
    pairs.push((
        "element count".to_string(),
        channel.element_count().to_string(),
    ));
    SignalInfo(pairs)
}

/// Subscribe a freshly connected channel with its native DBR_TIME_* type;
/// enums additionally get a one-shot DBR_GR_ENUM label fetch.  The label get
/// is issued first so its reply (in-order on the TCP connection) normally
/// arrives before the subscription's initial update.
fn subscribe(channel: &mut epics::Channel, state: &Arc<ChanState>) -> crate::EpicsResult<()> {
    let Some(field_type) = channel.field_type() else {
        // Raced with a disconnect; the next Connected event will retry.
        return Err(crate::EpicsError::General(
            "field type unavailable".to_string(),
        ));
    };
    let Some(dbr_type) = decode::time_dbr_for_field(field_type) else {
        return Err(crate::EpicsError::General(format!(
            "unsupported field type {field_type}"
        )));
    };

    if decode::is_enum_field(field_type) {
        let label_state = state.clone();
        channel.get_with_callback(
            sys::DBR_GR_ENUM,
            1,
            Box::new(move |args: epics::event_handler_args| {
                if !epics::event_succeeded(&args) {
                    return;
                }
                // SAFETY: a successful DBR_GR_ENUM get delivers a dbr_gr_enum.
                let labels = unsafe { decode::decode_gr_enum_labels(args.dbr) };
                if let Ok(mut cached) = label_state.labels.lock() {
                    *cached = labels;
                }
            }),
        )?;
    }

    let value_state = state.clone();
    channel.subscribe_simple(
        dbr_type,
        1, // arrays unsupported: element 0 only
        Box::new(move |args: epics::event_handler_args| {
            if !epics::event_succeeded(&args) {
                return;
            }
            let labels = value_state
                .labels
                .lock()
                .map(|cached| cached.clone())
                .unwrap_or_default();
            // SAFETY: CA delivers a DBR structure of the type we subscribed
            // with (args.type_), fully initialized.
            let value = unsafe { decode::decode_time_dbr(args.type_ as u32, args.dbr, &labels) };
            if let Some(value) = value {
                value_state.store_value(value);
            }
        }),
    )
}
