use std::{
    fs,
    path::PathBuf,
    time::{Duration, Instant},
};

use anyhow::{Context, anyhow};
use clap::Parser;
use evdev::{
    AttributeSet, AttributeSetRef, Device, EventSummary, EventType, InputEvent, RelativeAxisCode,
    uinput::VirtualDevice,
};
use gsf_core::{
    AccelMode, AllParamArgs, Param,
    persist::{ParamStore, SysFsStore},
    sensitivity,
};

#[derive(Parser, Debug)]
#[command(author, version, about = "host-side scroll acceleration daemon")]
struct Args {
    /// Path to source input device (/dev/input/eventX).
    #[arg(long)]
    device: Option<PathBuf>,

    /// Auto-select device by case-insensitive name substring.
    #[arg(long)]
    match_name: Option<String>,

    /// List candidate devices and exit.
    #[arg(long)]
    list_devices: bool,

    /// Don't create virtual output device, just print transformations.
    #[arg(long)]
    dry_run: bool,

    /// Do not EVIOCGRAB the source device.
    #[arg(long)]
    no_grab: bool,
}

#[derive(Debug, Default)]
struct AxisRuntime {
    last_event_at: Option<Instant>,
    remainder: f64,
    smoothed_speed: f64,
}

#[derive(Debug, Default)]
struct ScrollState {
    vertical: AxisRuntime,
    horizontal: AxisRuntime,
}

const SPEED_EMA_ALPHA: f64 = 0.25;
const MIN_DT_SECONDS: f64 = 0.004;
const SPEED_IDLE_RESET_SECONDS: f64 = 0.2;

fn axis_runtime_mut(state: &mut ScrollState, axis: RelativeAxisCode) -> &mut AxisRuntime {
    if axis == RelativeAxisCode::REL_HWHEEL || axis == RelativeAxisCode::REL_HWHEEL_HI_RES {
        &mut state.horizontal
    } else {
        &mut state.vertical
    }
}

fn main() -> anyhow::Result<()> {
    let args = Args::parse();

    if args.list_devices {
        print_devices();
        return Ok(());
    }

    let source_path = if let Some(path) = args.device {
        path
    } else {
        find_default_device(args.match_name.as_deref())
            .context("failed to auto-select a scroll-capable device")?
    };

    let mut source = Device::open(&source_path)
        .with_context(|| anyhow!("failed to open input device {}", source_path.display()))?;

    let source_name = source.name().unwrap_or("unknown").to_string();
    let has_scroll = source
        .supported_relative_axes()
        .map(has_scroll_axes)
        .unwrap_or(false);

    if !has_scroll {
        anyhow::bail!(
            "selected source device does not expose REL_WHEEL/REL_HWHEEL axes: {} ({})",
            source_path.display(),
            source_name
        );
    }

    if !args.no_grab {
        source
            .grab()
            .with_context(|| anyhow!("failed to grab {}", source_path.display()))?;
    }

    eprintln!(
        "using source device: {} ({})",
        source_path.display(),
        source_name
    );

    let mut virtual_dev = if args.dry_run {
        None
    } else {
        Some(build_virtual_from_source(&source).context("failed to create uinput device")?)
    };

    let store = SysFsStore;
    let mut state = ScrollState::default();
    let mut last_cfg_reload = Instant::now();
    let mut cfg = snapshot_config(&store)?;

    loop {
        if last_cfg_reload.elapsed() >= Duration::from_millis(100) {
            cfg = snapshot_config(&store)?;
            last_cfg_reload = Instant::now();
        }

        let events = source
            .fetch_events()
            .context("failed to read input events")?
            .collect::<Vec<_>>();
        let mut out_events = Vec::new();

        let suppress_lowres_v = events
            .iter()
            .any(|ev| ev.code() == RelativeAxisCode::REL_WHEEL_HI_RES.0);
        let suppress_lowres_h = events
            .iter()
            .any(|ev| ev.code() == RelativeAxisCode::REL_HWHEEL_HI_RES.0);

        for ev in events {
            if let Some(new_ev) = transform_event(
                ev,
                &cfg,
                &mut state,
                args.dry_run,
                suppress_lowres_v,
                suppress_lowres_h,
            ) {
                out_events.push(new_ev);
            }
        }

        if let Some(virtual_dev) = virtual_dev.as_mut()
            && !out_events.is_empty()
        {
            virtual_dev
                .emit(&out_events)
                .context("failed to emit event batch to uinput")?;
        }
    }
}

fn has_scroll_axes(axes: &AttributeSetRef<RelativeAxisCode>) -> bool {
    axes.contains(RelativeAxisCode::REL_WHEEL)
        || axes.contains(RelativeAxisCode::REL_WHEEL_HI_RES)
        || axes.contains(RelativeAxisCode::REL_HWHEEL)
        || axes.contains(RelativeAxisCode::REL_HWHEEL_HI_RES)
}

fn print_devices() {
    for (path, dev) in evdev::enumerate() {
        let has_scroll = dev
            .supported_relative_axes()
            .map(has_scroll_axes)
            .unwrap_or(false);

        eprintln!(
            "{}\t{}\t{}",
            path.display(),
            dev.name().unwrap_or("unknown"),
            if has_scroll {
                "[scroll-capable]"
            } else {
                "[no-scroll]"
            }
        );
    }
}

fn find_default_device(match_name: Option<&str>) -> anyhow::Result<PathBuf> {
    let needle = match_name.map(|s| s.to_ascii_lowercase());

    evdev::enumerate()
        .find_map(|(path, dev)| {
            let has_scroll = dev
                .supported_relative_axes()
                .map(has_scroll_axes)
                .unwrap_or(false);
            if !has_scroll {
                return None;
            }

            if let Some(needle) = needle.as_ref() {
                let name = dev.name().unwrap_or("unknown").to_ascii_lowercase();
                if !name.contains(needle) {
                    return None;
                }
            }

            Some(path)
        })
        .ok_or_else(|| anyhow!("no matching scroll-capable device found"))
}

fn build_virtual_from_source(source: &Device) -> anyhow::Result<VirtualDevice> {
    let name = format!("gsf virtual: {}", source.name().unwrap_or("unknown"));

    let mut rel_axes = AttributeSet::<RelativeAxisCode>::new();
    if let Some(axes) = source.supported_relative_axes() {
        for axis in axes.iter() {
            rel_axes.insert(axis);
        }
    }

    let mut builder = VirtualDevice::builder()?.name(name.as_bytes());

    if let Some(keys) = source.supported_keys() {
        builder = builder.with_keys(keys)?;
    }

    builder = builder.with_relative_axes(&rel_axes)?;

    let mut device = builder.build()?;

    for path in device.enumerate_dev_nodes_blocking()? {
        let path = path?;
        eprintln!("virtual device node: {}", path.display());
    }

    Ok(device)
}

fn snapshot_config(store: &SysFsStore) -> anyhow::Result<(AccelMode, AllParamArgs)> {
    let mode = store.get_current_accel_mode()?;

    let params = AllParamArgs {
        sens_mult: store.get(Param::SensMult)?,
        yx_ratio: store.get(Param::YxRatio)?,
        input_dpi: store.get(Param::InputDpi)?,
        accel: store.get(Param::Accel)?,
        offset_linear: store.get(Param::OffsetLinear)?,
        offset_natural: store.get(Param::OffsetNatural)?,
        output_cap: store.get(Param::OutputCap)?,
        decay_rate: store.get(Param::DecayRate)?,
        limit: store.get(Param::Limit)?,
        gamma: store.get(Param::Gamma)?,
        smooth: store.get(Param::Smooth)?,
        motivity: store.get(Param::Motivity)?,
        sync_speed: store.get(Param::SyncSpeed)?,
        angle_rotation: store.get(Param::AngleRotation)?,
    };

    Ok((mode, params))
}

fn transform_event(
    ev: InputEvent,
    cfg: &(AccelMode, AllParamArgs),
    state: &mut ScrollState,
    dry_run: bool,
    suppress_lowres_v: bool,
    suppress_lowres_h: bool,
) -> Option<InputEvent> {
    let (mode, params) = cfg;

    match ev.destructure() {
        EventSummary::RelativeAxis(_, axis, value)
            if axis == RelativeAxisCode::REL_WHEEL
                || axis == RelativeAxisCode::REL_WHEEL_HI_RES
                || axis == RelativeAxisCode::REL_HWHEEL
                || axis == RelativeAxisCode::REL_HWHEEL_HI_RES =>
        {
            if (axis == RelativeAxisCode::REL_WHEEL && suppress_lowres_v)
                || (axis == RelativeAxisCode::REL_HWHEEL && suppress_lowres_h)
            {
                return None;
            }
            let axis_state = axis_runtime_mut(state, axis);
            let now = Instant::now();
            let dt = axis_state
                .last_event_at
                .map(|t| now.saturating_duration_since(t).as_secs_f64())
                .unwrap_or(0.016)
                .max(MIN_DT_SECONDS);
            axis_state.last_event_at = Some(now);

            let axis_unit = if axis == RelativeAxisCode::REL_WHEEL_HI_RES
                || axis == RelativeAxisCode::REL_HWHEEL_HI_RES
            {
                120.0
            } else {
                1.0
            };

            // Normalize hi-res wheel units (120 per detent) to "detent-equivalent"
            // speed so tuning behaves similarly across REL_WHEEL and REL_WHEEL_HI_RES.
            let normalized_value = (value as f64) / axis_unit;
            let raw_speed = normalized_value.abs() / dt;

            // Smooth out bursty evdev timing so the live indicator and accel input
            // don't jump wildly on single tightly-packed events.
            let speed = if dt > SPEED_IDLE_RESET_SECONDS {
                raw_speed
            } else {
                SPEED_EMA_ALPHA * raw_speed + (1.0 - SPEED_EMA_ALPHA) * axis_state.smoothed_speed
            };
            axis_state.smoothed_speed = speed;

            let (gain_x, gain_y) = sensitivity(speed, *mode, params);
            let gain = if axis == RelativeAxisCode::REL_HWHEEL
                || axis == RelativeAxisCode::REL_HWHEEL_HI_RES
            {
                gain_x
            } else {
                gain_y
            };

            let scaled = (value as f64) * gain + axis_state.remainder;
            let quantized = scaled.trunc() as i32;
            axis_state.remainder = scaled - (quantized as f64);

            let _ = write_speed_stat(speed);

            if dry_run {
                eprintln!(
                    "scroll {:?}: in={} speed={:.2} gain={:.3} out={}",
                    axis, value, speed, gain, quantized
                );
            }

            if quantized == 0 {
                return None;
            }

            Some(InputEvent::new(EventType::RELATIVE.0, ev.code(), quantized))
        }
        _ => Some(ev),
    }
}

fn write_speed_stat(speed: f64) -> anyhow::Result<()> {
    let path = stats_path();
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent)
            .with_context(|| anyhow!("failed to create {}", parent.display()))?;
    }

    fs::write(&path, format!("{speed:.6}"))
        .with_context(|| anyhow!("failed to write {}", path.display()))?;
    Ok(())
}

fn stats_path() -> PathBuf {
    if let Ok(path) = std::env::var("GSF_STATS_PATH") {
        return PathBuf::from(path);
    }

    // Keep default shared across sudo/non-sudo runs.
    PathBuf::from("/tmp/gsf-speed.txt")
}
