use chrono::{
    DateTime, Datelike, Days, Local, LocalResult, NaiveDate, NaiveDateTime, NaiveTime, TimeZone,
    Timelike, Utc,
};
use log::{debug, error, info, trace, warn};
use serde::{Deserialize, Serialize};
use std::{
    fs,
    path::{Path, PathBuf},
    sync::Mutex,
};
use uuid::Uuid;

/// Duration (ms) an informational status message remains visible.
const STATUS_DURATION_MS: i64 = 4000;
/// Amount of time (ms) an alarm is considered to be "ringing" once triggered.
const RINGING_DURATION_MS: i64 = 6000;
/// Minimum timer duration accepted from the UI to avoid accidental zero timers.
const TIMER_MIN_MS: u64 = 1000;

/// Shared state container held by Tauri. It wraps a mutex-protected
/// [`StateInner`] and is responsible for persistence plus providing immutable
/// snapshots to the frontend.
pub struct AppState {
    inner: Mutex<StateInner>,
    storage_path: PathBuf,
}

impl AppState {
    /// Constructs a new state container and eagerly loads persisted alarms.
    pub fn new(storage_path: PathBuf) -> Self {
        let mut inner = StateInner::default();
        if let Some(list) = load_alarms(&storage_path) {
            let now = Local::now();
            inner.alarms = list
                .into_iter()
                .map(|alarm| Alarm {
                    id: alarm.id,
                    hour: alarm.hour,
                    minute: alarm.minute,
                    label: alarm.label,
                    active: alarm.active,
                    next_trigger: resolve_next_trigger(alarm.hour, alarm.minute, &now),
                    ringing_until: None,
                })
                .collect();
        }
        Self {
            inner: Mutex::new(inner),
            storage_path,
        }
    }

    /// Returns a `FrontendState` without mutating anything. Primarily used when
    /// the UI first boots.
    pub fn snapshot(&self) -> FrontendState {
        trace!("Building snapshot for frontend");
        self.apply(|_, _, _| {})
    }

    /// Executes the housekeeping refresh logic and returns an updated snapshot.
    pub fn tick(&self) -> FrontendState {
        trace!("Processing tick request");
        self.apply(|_, _, _| {})
    }

    /// Creates a new alarm entry using the provided time and label.
    pub fn create_alarm(&self, time: String, label: Option<String>) -> FrontendState {
        self.apply(|inner, now_ms, now_local| {
            inner.create_alarm(&time, label.as_deref(), now_ms, now_local);
        })
    }

    /// Toggles an existing alarm's active state.
    pub fn toggle_alarm(&self, id: String) -> FrontendState {
        self.apply(|inner, _now_ms, now_local| {
            inner.toggle_alarm(&id, now_local);
        })
    }

    /// Deletes an alarm permanently.
    pub fn delete_alarm(&self, id: String) -> FrontendState {
        self.apply(|inner, now_ms, _| {
            inner.delete_alarm(&id, now_ms);
        })
    }

    /// Starts or pauses the timer depending on the current state.
    pub fn toggle_timer(&self, input: Option<String>, force_parse: bool) -> FrontendState {
        self.apply(|inner, now_ms, _| {
            inner.toggle_timer(input.as_deref(), force_parse, now_ms);
        })
    }

    /// Resets the timer to its initial duration.
    pub fn reset_timer(&self) -> FrontendState {
        self.apply(|inner, _, _| inner.reset_timer())
    }

    /// Starts or pauses the stopwatch.
    pub fn toggle_stopwatch(&self) -> FrontendState {
        self.apply(|inner, now_ms, _| inner.toggle_stopwatch(now_ms))
    }

    /// Records a lap for the stopwatch if running.
    pub fn add_lap(&self) -> FrontendState {
        self.apply(|inner, now_ms, _| inner.add_lap(now_ms))
    }

    /// Resets the stopwatch to zero elapsed time and clears laps.
    pub fn reset_stopwatch(&self) -> FrontendState {
        self.apply(|inner, _, _| inner.reset_stopwatch())
    }

    /// Executes a state mutation closure while handling locking, refresh logic,
    /// snapshot generation, and persistence side effects.
    fn apply<F>(&self, mutator: F) -> FrontendState
    where
        F: FnOnce(&mut StateInner, i64, &DateTime<Local>),
    {
        let now_local = Local::now();
        let now_ms = now_local.timestamp_millis();
        let mut guard = self.inner.lock().expect("state mutex poisoned");
        trace!("Running state mutation (now_ms={})", now_ms);
        mutator(&mut guard, now_ms, &now_local);
        guard.refresh(now_ms, &now_local);
        let snapshot = guard.to_frontend(&now_local);
        let persisted = if guard.alarms_dirty {
            Some(
                guard
                    .alarms
                    .iter()
                    .map(PersistedAlarm::from)
                    .collect::<Vec<_>>(),
            )
        } else {
            None
        };
        guard.alarms_dirty = false;
        drop(guard);
        if let Some(list) = persisted {
            debug!("Persisting {} alarms to disk", list.len());
            self.save_alarms(&list);
        }
        snapshot
    }

    /// Persists the alarm list to disk, creating directories as needed.
    fn save_alarms(&self, alarms: &[PersistedAlarm]) {
        if let Some(parent) = self.storage_path.parent() {
            if let Err(error) = fs::create_dir_all(parent) {
                error!(
                    "Failed to create alarm storage directory {}: {error}",
                    parent.display()
                );
                return;
            }
        }
        let payload = AlarmStore {
            alarms: alarms.to_vec(),
        };
        match serde_json::to_string_pretty(&payload) {
            Ok(json) => {
                if let Err(error) = fs::write(&self.storage_path, json) {
                    error!(
                        "Failed to persist alarms to {}: {error}",
                        self.storage_path.display()
                    );
                } else {
                    debug!(
                        "Persisted {} alarms to {}",
                        payload.alarms.len(),
                        self.storage_path.display()
                    );
                }
            }
            Err(error) => {
                error!("Failed to serialize alarms: {error}");
            }
        }
    }
}

#[derive(Default)]
/// Mutable inner state containing alarms, timers, and transient UI messaging.
struct StateInner {
    alarms: Vec<Alarm>,
    timer: TimerState,
    stopwatch: StopwatchState,
    status: Option<StatusMessage>,
    play_chime: bool,
    alarms_dirty: bool,
}

impl StateInner {
    /// Executes periodic tasks that should run on every mutation or tick.
    fn refresh(&mut self, now_ms: i64, now_local: &DateTime<Local>) {
        self.process_alarms(now_ms, now_local);
        self.update_timer(now_ms);
        self.expire_status(now_ms);
    }

    /// Builds a full [`FrontendState`] representation of the current data.
    fn to_frontend(&mut self, now_local: &DateTime<Local>) -> FrontendState {
        let now_ms = now_local.timestamp_millis();
        let now_utc = now_local.with_timezone(&Utc);
        let clock = ClockState {
            local_time: format_time(now_local.hour(), now_local.minute(), now_local.second()),
            local_date: format_date(now_local.year(), now_local.month(), now_local.day()),
            utc_time: format_time(now_utc.hour(), now_utc.minute(), now_utc.second()),
            utc_date: format_date(now_utc.year(), now_utc.month(), now_utc.day()),
        };
        let alarms = self.build_alarm_view(now_ms);
        let timer = self.build_timer_view();
        let stopwatch = self.build_stopwatch_view(now_ms);
        let status = self.status.as_ref().map(|message| message.text.clone());
        let should_chime = std::mem::take(&mut self.play_chime);
        FrontendState {
            clock,
            alarms,
            timer,
            stopwatch,
            status,
            should_chime,
        }
    }

    /// Adds a new alarm and schedules its next trigger time.
    fn create_alarm(
        &mut self,
        raw_time: &str,
        label: Option<&str>,
        now_ms: i64,
        now_local: &DateTime<Local>,
    ) {
        let Some((hour, minute)) = parse_alarm_time(raw_time) else {
            warn!("Invalid alarm time input: {}", raw_time);
            self.push_status("時刻の形式が正しくありません", now_ms);
            return;
        };
        let label_text = label
            .map(|value| value.trim().to_string())
            .filter(|value| !value.is_empty())
            .unwrap_or_else(|| "アラーム".to_string());
        let alarm = Alarm {
            id: Uuid::new_v4().to_string(),
            hour,
            minute,
            label: label_text.clone(),
            active: true,
            next_trigger: resolve_next_trigger(hour, minute, now_local),
            ringing_until: None,
        };
        self.alarms.push(alarm);
        self.alarms_dirty = true;
        info!(
            "Alarm created: '{}' scheduled for {:02}:{:02}",
            label_text, hour, minute
        );
        self.push_status(&format!("アラーム追加: {}", label_text), now_ms);
    }

    /// Toggles an alarm's active state and recalculates its next trigger.
    fn toggle_alarm(&mut self, id: &str, now_local: &DateTime<Local>) {
        if let Some(alarm) = self.alarms.iter_mut().find(|alarm| alarm.id == id) {
            alarm.active = !alarm.active;
            alarm.ringing_until = None;
            alarm.next_trigger = resolve_next_trigger(alarm.hour, alarm.minute, now_local);
            self.alarms_dirty = true;
            info!(
                "Alarm {} toggled -> {}",
                id,
                if alarm.active { "active" } else { "inactive" }
            );
        } else {
            warn!("Requested toggle for unknown alarm id {}", id);
        }
    }

    /// Removes an alarm completely.
    fn delete_alarm(&mut self, id: &str, now_ms: i64) {
        if let Some(index) = self.alarms.iter().position(|alarm| alarm.id == id) {
            let removed = self.alarms.remove(index);
            self.alarms_dirty = true;
            info!("Alarm {} deleted", removed.id);
            self.push_status(&format!("アラーム削除: {}", removed.label), now_ms);
        } else {
            warn!("Requested delete for unknown alarm id {}", id);
        }
    }

    /// Handles timer start/pause logic, parsing inputs when necessary.
    fn toggle_timer(&mut self, input: Option<&str>, force_parse: bool, now_ms: i64) {
        if self.timer.running {
            if let Some(target) = self.timer.target_epoch_ms {
                self.timer.remaining_ms = target.saturating_sub(now_ms) as u64;
            }
            self.timer.running = false;
            self.timer.target_epoch_ms = None;
            info!("Timer paused with {}ms remaining", self.timer.remaining_ms);
            return;
        }

        let next_input = input.unwrap_or("").trim();
        let needs_parse = force_parse
            || self.timer.initial_ms == 0
            || (!next_input.is_empty() && Some(next_input) != self.timer.last_input.as_deref());

        if needs_parse {
            if next_input.is_empty() {
                warn!("Timer start requested without duration input");
                self.push_status("タイマーの時間を入力してください", now_ms);
                return;
            }
            let Some(duration) = parse_duration(next_input) else {
                warn!("Failed to parse timer duration from '{}'", next_input);
                self.push_status("時間の形式が正しくありません", now_ms);
                return;
            };
            if duration < TIMER_MIN_MS {
                warn!("Timer duration below minimum: {}ms", duration);
                self.push_status("1秒以上の時間を入力してください", now_ms);
                return;
            }
            self.timer.initial_ms = duration;
            self.timer.remaining_ms = duration;
            self.timer.last_input = Some(next_input.to_string());
        }

        if self.timer.remaining_ms == 0 {
            self.timer.remaining_ms = self.timer.initial_ms;
        }

        if self.timer.remaining_ms == 0 {
            self.push_status("1秒以上の時間を入力してください", now_ms);
            return;
        }

        self.timer.running = true;
        self.timer.target_epoch_ms = Some(now_ms + self.timer.remaining_ms as i64);
        info!(
            "Timer started for {}ms (target epoch {})",
            self.timer.remaining_ms,
            self.timer.target_epoch_ms.unwrap_or(now_ms)
        );
    }

    /// Restores the timer to its initial duration without starting it.
    fn reset_timer(&mut self) {
        self.timer.running = false;
        self.timer.target_epoch_ms = None;
        self.timer.remaining_ms = self.timer.initial_ms;
        info!("Timer reset to {}ms", self.timer.initial_ms);
    }

    /// Starts or pauses the stopwatch while accounting for elapsed time.
    fn toggle_stopwatch(&mut self, now_ms: i64) {
        if self.stopwatch.running {
            if let Some(start) = self.stopwatch.start_epoch_ms {
                let delta = now_ms.saturating_sub(start);
                self.stopwatch.elapsed_before_ms += delta as u64;
            }
            self.stopwatch.running = false;
            self.stopwatch.start_epoch_ms = None;
            info!("Stopwatch paused at {}ms", self.stopwatch.elapsed_before_ms);
        } else {
            self.stopwatch.running = true;
            self.stopwatch.start_epoch_ms = Some(now_ms);
            info!("Stopwatch started");
        }
    }

    /// Records a lap entry capturing both the total elapsed time and the delta
    /// since the previous lap.
    fn add_lap(&mut self, now_ms: i64) {
        let elapsed = self.stopwatch_elapsed(now_ms);
        if elapsed == 0 {
            debug!("Lap ignored because stopwatch has not started");
            return;
        }
        let previous_total = self
            .stopwatch
            .laps
            .last()
            .map(|lap| lap.total_ms)
            .unwrap_or(0);
        let entry = LapEntry {
            id: Uuid::new_v4().to_string(),
            total_ms: elapsed,
            delta_ms: elapsed.saturating_sub(previous_total),
        };
        self.stopwatch.laps.push(entry);
        debug!(
            "Lap recorded: {} (total {}ms)",
            self.stopwatch
                .laps
                .last()
                .map(|lap| lap.id.clone())
                .unwrap_or_default(),
            elapsed
        );
    }

    /// Stops the stopwatch and clears any lap history.
    fn reset_stopwatch(&mut self) {
        self.stopwatch.running = false;
        self.stopwatch.start_epoch_ms = None;
        self.stopwatch.elapsed_before_ms = 0;
        self.stopwatch.laps.clear();
        info!("Stopwatch reset");
    }

    /// Iterates configured alarms, triggering those scheduled for the current
    /// tick and scheduling their next activation.
    fn process_alarms(&mut self, now_ms: i64, now_local: &DateTime<Local>) {
        let mut triggered_labels = Vec::new();
        for alarm in &mut self.alarms {
            if let Some(until) = alarm.ringing_until {
                if until <= now_ms {
                    alarm.ringing_until = None;
                }
            }
            if !alarm.active {
                continue;
            }
            if now_ms >= alarm.next_trigger {
                alarm.ringing_until = Some(now_ms + RINGING_DURATION_MS);
                alarm.next_trigger = resolve_next_trigger(alarm.hour, alarm.minute, now_local);
                triggered_labels.push(alarm.label.clone());
            }
        }
        if !triggered_labels.is_empty() {
            self.play_chime = true;
            self.alarms_dirty = true;
            info!(
                "Triggered {} alarm(s): {:?}",
                triggered_labels.len(),
                triggered_labels
            );
            for label in triggered_labels {
                self.push_status(&format!("アラーム: {}", label), now_ms);
            }
        }
    }

    /// Updates timer countdown bookkeeping and fires completion status when the
    /// clock hits zero.
    fn update_timer(&mut self, now_ms: i64) {
        if self.timer.running {
            if let Some(target) = self.timer.target_epoch_ms {
                if now_ms >= target {
                    self.timer.running = false;
                    self.timer.target_epoch_ms = None;
                    self.timer.remaining_ms = 0;
                    self.play_chime = true;
                    info!("Timer completed");
                    self.push_status("タイマー終了", now_ms);
                } else {
                    self.timer.remaining_ms = target.saturating_sub(now_ms) as u64;
                }
            }
        }
    }

    /// Expires the active status message once its TTL elapses.
    fn expire_status(&mut self, now_ms: i64) {
        if let Some(status) = &self.status {
            if status.expires_at <= now_ms {
                self.status = None;
            }
        }
    }

    /// Builds the alarm section view model that the frontend expects.
    fn build_alarm_view(&self, now_ms: i64) -> AlarmSection {
        let mut sorted = self.alarms.iter().collect::<Vec<_>>();
        sorted.sort_by_key(|alarm| alarm.next_trigger);
        let items: Vec<AlarmView> = sorted
            .into_iter()
            .map(|alarm| AlarmView {
                id: alarm.id.clone(),
                label: alarm.label.clone(),
                time_label: format_time_label(alarm.hour, alarm.minute),
                active: alarm.active,
                ringing: alarm
                    .ringing_until
                    .map(|until| until > now_ms)
                    .unwrap_or(false),
                next_relative: format_relative(alarm.next_trigger, now_ms),
            })
            .collect();
        let active_alarms: Vec<&Alarm> = self.alarms.iter().filter(|alarm| alarm.active).collect();
        let next_summary = if active_alarms.is_empty() {
            "アクティブなアラームはありません".to_string()
        } else {
            let soonest = active_alarms
                .into_iter()
                .min_by_key(|alarm| alarm.next_trigger)
                .expect("active alarm available");
            format!(
                "次のアラーム: {} ({}) - {}",
                format_time_label(soonest.hour, soonest.minute),
                soonest.label,
                format_relative(soonest.next_trigger, now_ms)
            )
        };
        AlarmSection {
            items,
            next_summary,
        }
    }

    /// Builds the timer view model by formatting durations and button labels.
    fn build_timer_view(&self) -> TimerView {
        let display_ms = if self.timer.running {
            self.timer.remaining_ms
        } else if self.timer.remaining_ms > 0 {
            self.timer.remaining_ms
        } else {
            self.timer.initial_ms
        };
        let toggle_label = if self.timer.running {
            "一時停止"
        } else if self.timer.remaining_ms > 0 && self.timer.remaining_ms < self.timer.initial_ms {
            "再開"
        } else {
            "スタート"
        };
        let status_text = if self.timer.running {
            Some("カウント中".to_string())
        } else if self.timer.remaining_ms > 0 && self.timer.remaining_ms < self.timer.initial_ms {
            Some("一時停止".to_string())
        } else if !self.timer.running && self.timer.initial_ms > 0 && self.timer.remaining_ms == 0 {
            Some("完了".to_string())
        } else {
            None
        };
        TimerView {
            display: format_duration(display_ms),
            toggle_label: toggle_label.to_string(),
            status_text,
        }
    }

    /// Builds the stopwatch view model including lap breakdowns.
    fn build_stopwatch_view(&self, now_ms: i64) -> StopwatchView {
        let elapsed = self.stopwatch_elapsed(now_ms);
        let toggle_label = if self.stopwatch.running {
            "停止"
        } else {
            "スタート"
        };
        let laps = build_lap_views(&self.stopwatch.laps);
        StopwatchView {
            display: format_stopwatch(elapsed),
            toggle_label: toggle_label.to_string(),
            laps,
        }
    }

    /// Returns the total stopwatch elapsed milliseconds, optionally including
    /// the in-flight span if the stopwatch is currently running.
    fn stopwatch_elapsed(&self, now_ms: i64) -> u64 {
        self.stopwatch.elapsed_before_ms
            + if self.stopwatch.running {
                now_ms.saturating_sub(self.stopwatch.start_epoch_ms.unwrap_or(now_ms)) as u64
            } else {
                0
            }
    }

    /// Stores a transient status message that the UI can render for users.
    fn push_status(&mut self, message: &str, now_ms: i64) {
        self.status = Some(StatusMessage {
            text: message.to_string(),
            expires_at: now_ms + STATUS_DURATION_MS,
        });
    }
}

#[derive(Clone, Serialize, Deserialize)]
struct Alarm {
    id: String,
    hour: u8,
    minute: u8,
    label: String,
    active: bool,
    next_trigger: i64,
    ringing_until: Option<i64>,
}

#[derive(Default)]
struct TimerState {
    initial_ms: u64,
    remaining_ms: u64,
    running: bool,
    target_epoch_ms: Option<i64>,
    last_input: Option<String>,
}

#[derive(Default)]
struct StopwatchState {
    running: bool,
    start_epoch_ms: Option<i64>,
    elapsed_before_ms: u64,
    laps: Vec<LapEntry>,
}

#[derive(Clone)]
struct LapEntry {
    id: String,
    total_ms: u64,
    delta_ms: u64,
}

struct StatusMessage {
    text: String,
    expires_at: i64,
}

#[derive(Serialize)]
/// Comprehensive state payload consumed by the frontend.
pub struct FrontendState {
    pub clock: ClockState,
    pub alarms: AlarmSection,
    pub timer: TimerView,
    pub stopwatch: StopwatchView,
    pub status: Option<String>,
    pub should_chime: bool,
}

#[derive(Serialize)]
/// Clock label strings rendered in the UI.
pub struct ClockState {
    pub local_time: String,
    pub local_date: String,
    pub utc_time: String,
    pub utc_date: String,
}

#[derive(Serialize)]
/// Aggregated alarm listing plus small summary for the next trigger.
pub struct AlarmSection {
    pub items: Vec<AlarmView>,
    pub next_summary: String,
}

#[derive(Serialize)]
/// Individual alarm entry displayed in the frontend list.
pub struct AlarmView {
    pub id: String,
    pub label: String,
    pub time_label: String,
    pub active: bool,
    pub ringing: bool,
    pub next_relative: String,
}

#[derive(Serialize)]
/// Timer labels consumed by the UI controls.
pub struct TimerView {
    pub display: String,
    pub toggle_label: String,
    pub status_text: Option<String>,
}

#[derive(Serialize)]
/// Stopwatch value for the UI including lap breakdowns.
pub struct StopwatchView {
    pub display: String,
    pub toggle_label: String,
    pub laps: Vec<LapView>,
}

#[derive(Serialize)]
/// Single lap renderable information.
pub struct LapView {
    pub id: String,
    pub label: String,
    pub total: String,
    pub delta: String,
}

#[derive(Serialize, Deserialize, Clone)]
/// Minimal alarm representation stored on disk.
struct PersistedAlarm {
    id: String,
    hour: u8,
    minute: u8,
    label: String,
    active: bool,
}

#[derive(Serialize, Deserialize, Default)]
/// Wrapper used when serializing alarm lists to JSON.
struct AlarmStore {
    alarms: Vec<PersistedAlarm>,
}

impl From<&Alarm> for PersistedAlarm {
    fn from(value: &Alarm) -> Self {
        Self {
            id: value.id.clone(),
            hour: value.hour,
            minute: value.minute,
            label: value.label.clone(),
            active: value.active,
        }
    }
}

/// Attempts to load the alarm store JSON file from disk.
fn load_alarms(path: &Path) -> Option<Vec<PersistedAlarm>> {
    match fs::read(path) {
        Ok(bytes) => match serde_json::from_slice::<AlarmStore>(&bytes) {
            Ok(store) => {
                debug!("Loaded {} persisted alarms", store.alarms.len());
                Some(store.alarms)
            }
            Err(error) => {
                warn!("Failed to parse alarm store at {}: {error}", path.display());
                None
            }
        },
        Err(error) => {
            debug!(
                "No persisted alarms available at {} ({error})",
                path.display()
            );
            None
        }
    }
}

/// Formats a timestamp as HH:MM:SS.
fn format_time(hour: u32, minute: u32, second: u32) -> String {
    format!("{:02}:{:02}:{:02}", hour, minute, second)
}

/// Formats a date as YYYY-MM-DD.
fn format_date(year: i32, month: u32, day: u32) -> String {
    format!("{year:04}-{month:02}-{day:02}")
}

/// Formats an alarm display label.
fn format_time_label(hour: u8, minute: u8) -> String {
    format!("{:02}:{:02}", hour, minute)
}

/// Produces a human-friendly relative time string describing when an alarm
/// will next trigger.
fn format_relative(target_ms: i64, now_ms: i64) -> String {
    let diff = target_ms - now_ms;
    if diff <= 0 {
        return "まもなく".to_string();
    }
    if diff <= 60_000 {
        return "まもなく".to_string();
    }
    let minutes = diff / 60_000;
    if minutes < 60 {
        return format!("{}分後", minutes);
    }
    let hours = minutes / 60;
    let mins = minutes % 60;
    format!("{}時間{}分後", hours, mins)
}

/// Formats a duration for timer display, switching to HH:MM:SS when needed.
fn format_duration(ms: u64) -> String {
    let total_seconds = (ms / 1000) as u64;
    let minutes = total_seconds / 60;
    let seconds = total_seconds % 60;
    let hundredths = (ms % 1000) / 10;
    if minutes >= 60 {
        let hours = minutes / 60;
        let rem_minutes = minutes % 60;
        format!("{:02}:{:02}:{:02}", hours, rem_minutes, seconds)
    } else {
        format!("{:02}:{:02}.{:02}", minutes, seconds, hundredths)
    }
}

/// Formats stopwatch durations with millisecond precision.
fn format_stopwatch(ms: u64) -> String {
    let total_seconds = ms / 1000;
    let minutes = total_seconds / 60;
    let seconds = total_seconds % 60;
    let millis = ms % 1000;
    format!("{:02}:{:02}.{:03}", minutes, seconds, millis)
}

/// Converts raw lap entries into renderable view data.
fn build_lap_views(entries: &[LapEntry]) -> Vec<LapView> {
    if entries.is_empty() {
        return Vec::new();
    }
    let total = entries.len();
    entries
        .iter()
        .rev()
        .enumerate()
        .map(|(index, lap)| LapView {
            id: lap.id.clone(),
            label: format!("Lap {}", total - index),
            total: format!("合計 {}", format_stopwatch(lap.total_ms)),
            delta: format!("+{}", format_stopwatch(lap.delta_ms)),
        })
        .collect()
}

/// Parses HH:MM strings emitted by the UI.
fn parse_alarm_time(value: &str) -> Option<(u8, u8)> {
    let trimmed = value.trim();
    let mut parts = trimmed.split(':');
    let hour = parts.next()?.parse::<u8>().ok()?;
    let minute = parts.next()?.parse::<u8>().ok()?;
    if parts.next().is_some() || hour > 23 || minute > 59 {
        return None;
    }
    Some((hour, minute))
}

/// Parses timer duration input supporting seconds, MM:SS, or `1h 30m` formats.
fn parse_duration(value: &str) -> Option<u64> {
    let trimmed = value.trim();
    if trimmed.is_empty() {
        return None;
    }
    if trimmed.chars().all(|ch| ch.is_ascii_digit()) {
        return trimmed.parse::<u64>().ok().map(|seconds| seconds * 1000);
    }
    if trimmed.contains(':') {
        let mut parts = trimmed.split(':');
        let minutes = parts.next()?.parse::<u64>().ok()?;
        let seconds = parts.next()?.parse::<u64>().ok()?;
        if parts.next().is_some() {
            return None;
        }
        return Some((minutes * 60 + seconds) * 1000);
    }
    parse_unit_duration(trimmed)
}

/// Helper that parses unit-suffixed duration tokens like `10m 5s`.
fn parse_unit_duration(value: &str) -> Option<u64> {
    let mut total_ms = 0u64;
    for chunk in value.split_whitespace() {
        if chunk.is_empty() {
            continue;
        }
        let split_index = chunk.find(|c: char| !c.is_ascii_digit())?;
        let (number_part, unit_part) = chunk.split_at(split_index);
        if number_part.is_empty() {
            return None;
        }
        let amount = number_part.parse::<u64>().ok()?;
        let unit = unit_part.chars().next()?.to_ascii_lowercase();
        match unit {
            'h' => total_ms += amount * 3_600_000,
            'm' => total_ms += amount * 60_000,
            's' => total_ms += amount * 1000,
            _ => return None,
        }
    }
    if total_ms > 0 {
        Some(total_ms)
    } else {
        None
    }
}

/// Calculates the next trigger timestamp in milliseconds for an alarm.
fn resolve_next_trigger(hour: u8, minute: u8, now_local: &DateTime<Local>) -> i64 {
    let naive_time = NaiveTime::from_hms_opt(hour as u32, minute as u32, 0)
        .unwrap_or_else(|| NaiveTime::from_hms_opt(0, 0, 0).unwrap());
    let mut candidate = combine_local(now_local.date_naive(), naive_time);
    if candidate.timestamp_millis() <= now_local.timestamp_millis() {
        let next_day = now_local.date_naive() + Days::new(1);
        candidate = combine_local(next_day, naive_time);
    }
    candidate.timestamp_millis()
}

/// Safely combines a date and time within the Local timezone, falling back to
/// UTC if the local conversion is ambiguous or invalid.
fn combine_local(date: NaiveDate, time: NaiveTime) -> DateTime<Local> {
    let naive = NaiveDateTime::new(date, time);
    match Local.from_local_datetime(&naive) {
        LocalResult::Single(value) => value,
        LocalResult::Ambiguous(first, _second) => first,
        LocalResult::None => {
            DateTime::<Utc>::from_naive_utc_and_offset(naive, Utc).with_timezone(&Local)
        }
    }
}
