use plc_comm_hostlink::{
    HostLinkConnectionOptions, HostLinkError, HostLinkValue, KvDeviceAddress, KvDeviceRangeEntry,
    KvDeviceRangeSegment, QueuedHostLinkClient, open_and_connect,
};
use std::collections::BTreeSet;
use std::error::Error;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ValueKind {
    Bit,
    Word,
    Dword,
}

#[derive(Debug, Default)]
struct Summary {
    passed: usize,
    read_failed: usize,
    write_failed: usize,
    readback_failed: usize,
    restore_failed: usize,
    skipped: usize,
    unsupported: usize,
}

impl Summary {
    fn is_success(&self) -> bool {
        self.read_failed == 0
            && self.write_failed == 0
            && self.readback_failed == 0
            && self.restore_failed == 0
    }
}

#[derive(Debug)]
struct Failure {
    address: String,
    phase: &'static str,
    message: String,
}

#[derive(Debug)]
struct DeviceReport {
    device: String,
    address_range: String,
    value_kind: Option<ValueKind>,
    sample_addresses: Vec<String>,
    failures: Vec<Failure>,
    passed: usize,
    read_failed: usize,
    write_failed: usize,
    readback_failed: usize,
    restore_failed: usize,
    skipped: usize,
    unsupported: usize,
    untested_reason: Option<String>,
}

impl DeviceReport {
    fn new(entry: &KvDeviceRangeEntry) -> Self {
        Self {
            device: entry.device.clone(),
            address_range: entry
                .address_range
                .clone()
                .unwrap_or_else(|| "n/a".to_owned()),
            value_kind: None,
            sample_addresses: Vec::new(),
            failures: Vec::new(),
            passed: 0,
            read_failed: 0,
            write_failed: 0,
            readback_failed: 0,
            restore_failed: 0,
            skipped: 0,
            unsupported: 0,
            untested_reason: None,
        }
    }

    fn fail(&mut self, address: String, phase: &'static str, message: String) {
        self.failures.push(Failure {
            address,
            phase,
            message,
        });
    }
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
    let args = std::env::args().collect::<Vec<_>>();
    let host = args
        .get(1)
        .cloned()
        .or_else(|| std::env::var("HOSTLINK_HOST").ok())
        .unwrap_or_else(|| "192.168.250.100".to_owned());
    let port = args
        .get(2)
        .cloned()
        .or_else(|| std::env::var("HOSTLINK_PORT").ok())
        .unwrap_or_else(|| "8501".to_owned())
        .parse::<u16>()?;
    let sample_points = env_usize("KV_SAMPLE_POINTS", 10);
    let only = env_csv("KV_SAMPLE_ONLY");
    let only_set = only.iter().cloned().collect::<BTreeSet<_>>();

    let mut options = HostLinkConnectionOptions::new(host);
    options.port = port;
    let client = open_and_connect(options).await?;
    let catalog = client.read_device_range_catalog().await?;

    println!(
        "catalog -> model={} resolved_model={} model_code={} sample_points={}",
        catalog.model, catalog.resolved_model, catalog.model_code, sample_points
    );
    if !only.is_empty() {
        println!("only -> {}", only.join(","));
    }

    let mut summary = Summary::default();
    for entry in &catalog.entries {
        if !only_set.is_empty() && !matches_only(entry, &only_set) {
            continue;
        }

        let mut report = DeviceReport::new(entry);
        run_entry(&client, entry, sample_points, &mut summary, &mut report).await;
        print_device_report(&report);
    }

    let _ = client.close().await;
    println!(
        "summary -> passed={} read_failed={} write_failed={} readback_failed={} restore_failed={} skipped={} unsupported={}",
        summary.passed,
        summary.read_failed,
        summary.write_failed,
        summary.readback_failed,
        summary.restore_failed,
        summary.skipped,
        summary.unsupported
    );

    if summary.restore_failed > 0 {
        return Err(make_error("one or more restore operations failed"));
    }
    if !summary.is_success() {
        return Err(make_error("one or more device sample checks failed"));
    }

    Ok(())
}

async fn run_entry(
    client: &QueuedHostLinkClient,
    entry: &KvDeviceRangeEntry,
    sample_points: usize,
    summary: &mut Summary,
    report: &mut DeviceReport,
) {
    if !entry.supported {
        skip_device(summary, report, "unsupported by catalog");
        return;
    }
    if entry.segments.is_empty() {
        skip_device(summary, report, "catalog has no concrete segment");
        return;
    }

    let kind = kind_for(entry);
    report.value_kind = Some(kind);
    let mut any_sample = false;

    for segment in &entry.segments {
        let Some(upper_bound) = effective_upper_bound(segment, kind) else {
            skip_device(summary, report, "open-ended or too narrow range");
            return;
        };
        let lower_bound = effective_lower_bound(segment);
        if lower_bound > upper_bound {
            skip_device(summary, report, "effective sample range is empty");
            return;
        }

        let samples =
            sample_numbers_for_segment(segment, lower_bound, upper_bound, kind, sample_points);
        if samples.is_empty() {
            skip_device(
                summary,
                report,
                "no valid sample address could be generated",
            );
            return;
        }

        for number in samples {
            let address = match format_address(&segment.device, number) {
                Ok(address) => address,
                Err(error) => {
                    summary.unsupported += 1;
                    report.unsupported += 1;
                    report.untested_reason =
                        Some(format!("parser/client does not support {}", segment.device));
                    report.fail(segment.device.clone(), "unsupported", error.to_string());
                    return;
                }
            };

            any_sample = true;
            report.sample_addresses.push(address.clone());
            match exercise_point(client, &address, kind).await {
                Ok(()) => {
                    summary.passed += 1;
                    report.passed += 1;
                }
                Err((phase, message)) if phase == "read" => {
                    summary.read_failed += 1;
                    report.read_failed += 1;
                    report.fail(address, phase, message);
                }
                Err((phase, message)) if phase == "restore" => {
                    summary.restore_failed += 1;
                    report.restore_failed += 1;
                    report.fail(address, phase, message);
                }
                Err((phase, message)) if phase == "readback" => {
                    summary.readback_failed += 1;
                    report.readback_failed += 1;
                    report.fail(address, phase, message);
                }
                Err((phase, message)) => {
                    summary.write_failed += 1;
                    report.write_failed += 1;
                    report.fail(address, phase, message);
                }
            }
        }
    }

    if !any_sample {
        skip_device(
            summary,
            report,
            "no valid sample address could be generated",
        );
    }
}

fn skip_device(summary: &mut Summary, report: &mut DeviceReport, reason: &str) {
    summary.skipped += 1;
    report.skipped += 1;
    report.untested_reason = Some(reason.to_owned());
}

fn print_device_report(report: &DeviceReport) {
    if let Some(reason) = &report.untested_reason {
        if report.sample_addresses.is_empty() {
            println!("UNTESTED {}: {reason}", report.device);
            for failure in &report.failures {
                println!(
                    "FAIL {} {}: {}",
                    failure.phase, failure.address, failure.message
                );
            }
            return;
        }
    }

    println!(
        "DEVICE {} range={} kind={:?} samples={}",
        report.device,
        report.address_range,
        report.value_kind,
        report.sample_addresses.len()
    );
    for address in &report.sample_addresses {
        println!("SAMPLE {address}");
    }
    for failure in &report.failures {
        println!(
            "FAIL {} {}: {}",
            failure.phase, failure.address, failure.message
        );
    }
    println!(
        "DEVICE-SUMMARY {} passed={} read_failed={} write_failed={} readback_failed={} restore_failed={} skipped={} unsupported={}",
        report.device,
        report.passed,
        report.read_failed,
        report.write_failed,
        report.readback_failed,
        report.restore_failed,
        report.skipped,
        report.unsupported
    );
}

async fn exercise_point(
    client: &QueuedHostLinkClient,
    address: &str,
    kind: ValueKind,
) -> Result<(), (&'static str, String)> {
    let original = read_value(client, address, kind)
        .await
        .map_err(|error| ("read", error.to_string()))?;
    let (value_a, value_b) = test_values(address, &original, kind);
    let mut restore_needed = false;

    let test_result: Result<(), (&'static str, String)> = async {
        write_value(client, address, kind, value_a.clone())
            .await
            .map_err(|error| ("write", error.to_string()))?;
        restore_needed = true;
        assert_value(client, address, kind, &value_a).await?;
        write_value(client, address, kind, value_b.clone())
            .await
            .map_err(|error| ("write", error.to_string()))?;
        assert_value(client, address, kind, &value_b).await?;
        Ok(())
    }
    .await;

    let restore_result = if restore_needed {
        Some(write_value(client, address, kind, original).await)
    } else {
        None
    };
    match (test_result, restore_result) {
        (Ok(()), Some(Ok(()))) | (Ok(()), None) => Ok(()),
        (Ok(()), Some(Err(error))) => Err(("restore", error.to_string())),
        (Err((phase, message)), Some(Ok(()))) | (Err((phase, message)), None) => {
            Err((phase, message))
        }
        (Err((_phase, test_error)), Some(Err(restore_error))) => Err((
            "restore",
            format!("{test_error}; restore also failed: {restore_error}"),
        )),
    }
}

async fn assert_value(
    client: &QueuedHostLinkClient,
    address: &str,
    kind: ValueKind,
    expected: &HostLinkValue,
) -> Result<(), (&'static str, String)> {
    let observed = read_value(client, address, kind)
        .await
        .map_err(|error| ("readback", error.to_string()))?;
    if &observed != expected {
        return Err((
            "readback",
            format!("readback mismatch: expected={expected:?} observed={observed:?}"),
        ));
    }
    Ok(())
}

async fn read_value(
    client: &QueuedHostLinkClient,
    address: &str,
    kind: ValueKind,
) -> Result<HostLinkValue, HostLinkError> {
    client.read_typed(address, dtype_for(kind)).await
}

async fn write_value(
    client: &QueuedHostLinkClient,
    address: &str,
    kind: ValueKind,
    value: HostLinkValue,
) -> Result<(), HostLinkError> {
    client.write_typed(address, dtype_for(kind), value).await
}

fn kind_for(entry: &KvDeviceRangeEntry) -> ValueKind {
    if entry.is_bit_device {
        ValueKind::Bit
    } else if matches!(
        entry.device_type.as_str(),
        "T" | "TC" | "TS" | "C" | "CC" | "CS"
    ) {
        ValueKind::Dword
    } else {
        ValueKind::Word
    }
}

fn dtype_for(kind: ValueKind) -> &'static str {
    match kind {
        ValueKind::Bit => "",
        ValueKind::Word => "U",
        ValueKind::Dword => "D",
    }
}

fn test_values(
    address: &str,
    original: &HostLinkValue,
    kind: ValueKind,
) -> (HostLinkValue, HostLinkValue) {
    match (kind, original) {
        (ValueKind::Bit, HostLinkValue::Bool(value)) => {
            (HostLinkValue::Bool(!*value), HostLinkValue::Bool(*value))
        }
        (ValueKind::Word, HostLinkValue::U16(original)) => {
            let mut a = seeded_u16(address, 0x1111);
            let mut b = seeded_u16(address, 0x2222);
            if a == *original {
                a ^= 0x00FF;
            }
            if b == a {
                b ^= 0xFF00;
            }
            (HostLinkValue::U16(a), HostLinkValue::U16(b))
        }
        (ValueKind::Dword, HostLinkValue::U32(original)) => {
            let mut a = seeded_u32(address, 0x3333);
            let mut b = seeded_u32(address, 0x4444);
            if a == *original {
                a ^= 0x0000_FFFF;
            }
            if b == a {
                b ^= 0xFFFF_0000;
            }
            (HostLinkValue::U32(a), HostLinkValue::U32(b))
        }
        _ => {
            let a = seeded_u16(address, 0x5555);
            let b = seeded_u16(address, 0x6666);
            (HostLinkValue::U16(a), HostLinkValue::U16(b))
        }
    }
}

fn effective_lower_bound(segment: &KvDeviceRangeSegment) -> u32 {
    if segment.device.eq_ignore_ascii_case("R") {
        segment.lower_bound.max(200)
    } else {
        segment.lower_bound
    }
}

fn effective_upper_bound(segment: &KvDeviceRangeSegment, kind: ValueKind) -> Option<u32> {
    let upper = segment.upper_bound?;
    if kind == ValueKind::Dword {
        if upper <= segment.lower_bound {
            return None;
        }
        Some(upper - 1)
    } else {
        Some(upper)
    }
}

fn sample_numbers_for_segment(
    segment: &KvDeviceRangeSegment,
    lower_bound: u32,
    upper_bound: u32,
    kind: ValueKind,
    count: usize,
) -> Vec<u32> {
    let mut selected = BTreeSet::new();
    for number in sample_numbers_between(lower_bound, upper_bound, count) {
        if let Some(number) =
            normalize_sample_number(&segment.device, number, lower_bound, upper_bound, kind)
        {
            selected.insert(number);
        }
    }

    let mut cursor = lower_bound;
    while selected.len() < count && cursor <= upper_bound {
        if let Some(number) =
            normalize_sample_number(&segment.device, cursor, lower_bound, upper_bound, kind)
        {
            selected.insert(number);
        }
        cursor = cursor.saturating_add(1);
        if cursor == u32::MAX {
            break;
        }
    }

    selected.into_iter().collect()
}

fn sample_numbers_between(lower_bound: u32, upper_bound: u32, count: usize) -> Vec<u32> {
    if count == 0 || lower_bound > upper_bound {
        return Vec::new();
    }

    let span = upper_bound - lower_bound;
    if (span as u64) < count as u64 {
        return (lower_bound..=upper_bound).collect();
    }

    let lower = lower_bound as u64;
    let span = span as u64;
    let mut selected = BTreeSet::new();
    for offset in [
        0,
        span,
        span / 2,
        span / 4,
        (span * 3) / 4,
        span / 8,
        (span * 3) / 8,
        (span * 5) / 8,
        (span * 7) / 8,
        1,
        span.saturating_sub(1),
    ] {
        if selected.len() < count {
            selected.insert((lower + offset) as u32);
        }
    }

    for index in 0..count {
        if selected.len() >= count {
            break;
        }
        let denominator = count.saturating_sub(1) as u64;
        let offset = (index as u64 * span).checked_div(denominator).unwrap_or(0);
        selected.insert((lower + offset) as u32);
    }

    selected.into_iter().collect()
}

fn normalize_sample_number(
    device_type: &str,
    number: u32,
    lower_bound: u32,
    upper_bound: u32,
    _kind: ValueKind,
) -> Option<u32> {
    let number = number.clamp(lower_bound, upper_bound);
    if !uses_bit_bank_address(device_type) {
        return Some(number);
    }

    nearest_valid_bit_bank_number(number, lower_bound, upper_bound)
}

fn nearest_valid_bit_bank_number(number: u32, lower_bound: u32, upper_bound: u32) -> Option<u32> {
    let bank = number / 100;
    let bit = (number % 100).min(15);
    let mut candidates = Vec::new();
    for candidate_bank in [
        bank,
        bank.saturating_add(1),
        bank.saturating_sub(1),
        lower_bound / 100,
        upper_bound / 100,
    ] {
        for candidate_bit in [bit, 0, 15] {
            let candidate = candidate_bank
                .checked_mul(100)
                .and_then(|base| base.checked_add(candidate_bit))?;
            if candidate >= lower_bound && candidate <= upper_bound && candidate % 100 <= 15 {
                candidates.push(candidate);
            }
        }
    }

    candidates
        .into_iter()
        .min_by_key(|candidate| candidate.abs_diff(number))
}

fn uses_bit_bank_address(device_type: &str) -> bool {
    matches!(device_type, "R" | "MR" | "LR" | "CR")
}

fn format_address(device_type: &str, number: u32) -> Result<String, HostLinkError> {
    KvDeviceAddress {
        device_type: device_type.to_ascii_uppercase(),
        number,
        suffix: String::new(),
    }
    .to_text()
}

fn seeded_u16(label: &str, salt: u32) -> u16 {
    let mut hash = 0x811C9DC5u32 ^ salt;
    for byte in label.as_bytes() {
        hash ^= u32::from(*byte);
        hash = hash.wrapping_mul(0x0100_0193);
    }
    let value = ((hash & 0xFFFF) as u16) | 1;
    if value == 0 { 1 } else { value }
}

fn seeded_u32(label: &str, salt: u32) -> u32 {
    let high = seeded_u16(label, salt) as u32;
    let low = seeded_u16(label, salt ^ 0xA5A5_5A5A) as u32;
    (high << 16) | low
}

fn env_usize(name: &str, default: usize) -> usize {
    std::env::var(name)
        .ok()
        .and_then(|value| value.parse::<usize>().ok())
        .filter(|value| *value > 0)
        .unwrap_or(default)
}

fn env_csv(name: &str) -> Vec<String> {
    std::env::var(name)
        .unwrap_or_default()
        .split(',')
        .map(|value| value.trim().to_ascii_uppercase())
        .filter(|value| !value.is_empty())
        .collect()
}

fn matches_only(entry: &KvDeviceRangeEntry, only: &BTreeSet<String>) -> bool {
    only.contains(&entry.device.to_ascii_uppercase())
        || only.contains(&entry.device_type.to_ascii_uppercase())
        || entry
            .segments
            .iter()
            .any(|segment| only.contains(&segment.device.to_ascii_uppercase()))
}

fn make_error(message: impl Into<String>) -> Box<dyn Error> {
    Box::new(std::io::Error::other(message.into()))
}
