use crate::serial_core::codecs::tactile_a::{
    export_recording_csv, TactileACodec, TactileACsvImporter, TactileAHandler,
};
use crate::serial_core::error::SerialError;
use crate::serial_core::record::CsvImporter;
use crate::serial_core::serial::{PollMode, TactileAPollRequester};
use crate::serial_core::{serial, TactileARecording};
use log::info;
use serde::Serialize;
use std::fs::File;
use std::io::Cursor;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use tauri::{async_runtime::JoinHandle, AppHandle, Manager, State};
use tokio_serial::{available_ports, SerialPortBuilderExt};
use tokio_util::sync::CancellationToken;

const DEFAULT_TACTILE_COLS: usize = 7;
const DEFAULT_TACTILE_ROWS: usize = 12;
const DEFAULT_TACTILE_POLL_INTERVAL_MS: u64 = 10;
const DEFAULT_TACTILE_REPLY_TIMEOUT_MS: u64 = 140;

type SharedTactileRecording = Arc<Mutex<TactileARecording>>;


#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SerialConnectResponse {
    pub port: String,
    pub connected: bool,
    pub message: String,
}

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SerialExportResponse {
    pub path: String,
    pub frame_count: usize,
    pub message: String,
}

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SerialImportFrame {
    pub data: Vec<i32>,
    pub dts_ms: u64,
}

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SerialImportResponse {
    pub file_name: String,
    pub frame_count: usize,
    pub channel_count: usize,
    pub frames: Vec<SerialImportFrame>,
    pub message: String,
}

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SerialRecordStateResponse {
    pub has_data: bool,
    pub frame_count: usize,
}

struct SerialSession {
    port: String,
    cancel: CancellationToken,
    task: JoinHandle<()>,
    current_record: SharedTactileRecording,
}

#[derive(Default)]
pub struct SerialConnectionState {
    session: Mutex<Option<SerialSession>>,
    last_record: Mutex<Option<SharedTactileRecording>>
}

pub async fn shutdown_active_session(
    state: &SerialConnectionState,
) -> Result<Option<(String, SharedTactileRecording)>, SerialError> {
    let session = {
        let mut guard = state.session.lock().map_err(|_| SerialError::StateError)?;
        guard.take()
    };

    let Some(SerialSession {
        port,
        cancel,
        task,
        current_record,
    }) = session else {
        return Ok(None);
    };

    cancel.cancel();
    let _ = task.await;

    let frame_count = current_record
        .lock()
        .map(|record| record.frames.len())
        .unwrap_or(0);

    info!("last_record has {} frames", frame_count);

    if let Ok(mut last_record) = state.last_record.lock() {
        *last_record = Some(current_record.clone());
    }

    Ok(Some((port, current_record)))
}

#[tauri::command]
pub fn serial_enum() -> Result<Vec<String>, SerialError> {
    let ports = available_ports()
        .map_err(|_| SerialError::ScanError)?
        .into_iter()
        .map(|info| info.port_name)
        .collect();

    Ok(ports)
}

#[tauri::command]
pub async fn serial_connect(
    app: AppHandle,
    port: String,
    state: State<'_, SerialConnectionState>,
) -> Result<SerialConnectResponse, SerialError> {
    let port_name = port.trim().to_string();
    if port_name.is_empty() {
        return Err(SerialError::InvalidConfig);
    }

    {
        let session = state.session.lock().map_err(|_| SerialError::StateError)?;
        if session.is_some() {
            return Err(SerialError::AlreadyConnected);
        }
    }

    let cancel = CancellationToken::new();
    let current_record = Arc::new(Mutex::new(TactileARecording::new()));
    let task_record = current_record.clone();
    let task_cancel = cancel.clone();
    let task_app = app.clone();
    let task_port_name = port_name.clone();

    let port = tokio_serial::new(&port_name, 921600)
        .open_native_async()
        .map_err(|_| SerialError::OpenError)?;
    let session_started_at = Instant::now();

    let task = tauri::async_runtime::spawn(async move {
        let codec = TactileACodec::new(DEFAULT_TACTILE_COLS, DEFAULT_TACTILE_ROWS);
        let handler = TactileAHandler;
        let poll_mode = PollMode::Enabled(Box::new(TactileAPollRequester::new(
            Duration::from_millis(DEFAULT_TACTILE_POLL_INTERVAL_MS),
            DEFAULT_TACTILE_COLS,
            DEFAULT_TACTILE_ROWS,
            Duration::from_millis(DEFAULT_TACTILE_REPLY_TIMEOUT_MS),
        )));

        if let Err(error) = serial::run_serial_with_poll(
            task_app.clone(),
            port,
            codec,
            handler,
            session_started_at,
            task_record.clone(),
            task_cancel,
            poll_mode,
        )
        .await
        {
            eprintln!("serial task exited with error: {error}");
        }

        let manager = task_app.state::<SerialConnectionState>();
        if let Ok(mut last_record) = manager.last_record.lock() {
            *last_record = Some(task_record);
        }

        let mut session = match manager.session.lock() {
            Ok(session) => session,
            Err(_) => return,
        };

        {
            let should_clear = session
                .as_ref()
                .map(|current| current.port.as_str() == task_port_name.as_str())
                .unwrap_or(false);

            if should_clear {
                session.take();
            }
        }
    });

    let mut session = state.session.lock().map_err(|_| SerialError::StateError)?;
    if session.is_some() {
        cancel.cancel();
        task.abort();
        return Err(SerialError::AlreadyConnected);
    }

    *session = Some(SerialSession {
        port: port_name.clone(),
        cancel,
        task,
        current_record
    });

    Ok(SerialConnectResponse {
        port: port_name,
        connected: true,
        message: "connected".to_string(),
    })
}

#[tauri::command]
pub async fn serial_disconnect(
    state: State<'_, SerialConnectionState>,
) -> Result<SerialConnectResponse, SerialError> {
    let Some((port, _current_record)) = shutdown_active_session(&state).await?
    else {
        return Ok(SerialConnectResponse {
            port: String::new(),
            connected: false,
            message: "already disconnected".to_string(),
        });
    };

    Ok(SerialConnectResponse {
        port,
        connected: false,
        message: "disconnected".to_string(),
    })
}

#[tauri::command]
pub fn serial_export_csv(
    app: AppHandle,
    state: State<'_, SerialConnectionState>,
) -> Result<SerialExportResponse, SerialError> {
    let mut output_dir = match app.path().desktop_dir() {
        Ok(path) => path,
        Err(_) => std::env::current_dir().map_err(|_| SerialError::ExportError)?,
    };

    let timestamp = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|duration| duration.as_millis())
        .unwrap_or_default();

    output_dir.push(format!("joyson_export_{timestamp}.csv"));
    let record = resolve_record_for_export(&state)?;
    let frame_count = write_record_to_csv(record, &output_dir)?;

    let path = output_dir.display().to_string();
    info!("csv exported to {path}, frame_count={frame_count}");

    Ok(SerialExportResponse {
        path,
        frame_count,
        message: "exported".to_string(),
    })
}

#[tauri::command]
pub fn serial_has_record_data(
    state: State<'_, SerialConnectionState>,
) -> Result<SerialRecordStateResponse, SerialError> {
    let frame_count = snapshot_record_frame_count(&state)?;

    Ok(SerialRecordStateResponse {
        has_data: frame_count > 0,
        frame_count,
    })
}

#[tauri::command]
pub fn serial_export_csv_to_path(
    file_path: String,
    state: State<'_, SerialConnectionState>,
) -> Result<SerialExportResponse, SerialError> {
    let output_path = resolve_export_path(file_path)?;
    let record = resolve_record_for_export(&state)?;
    let frame_count = write_record_to_csv(record, &output_path)?;
    let path = output_path.display().to_string();

    info!("csv exported to {path}, frame_count={frame_count}");

    Ok(SerialExportResponse {
        path,
        frame_count,
        message: "exported".to_string(),
    })
}

#[tauri::command]
pub fn serial_import_csv(file_name: String, csv_content: String) -> Result<SerialImportResponse, SerialError> {
    let mut importer = TactileACsvImporter::new(file_name.as_str());
    let packets = importer
        .load(Cursor::new(csv_content.into_bytes()))
        .map_err(|_| SerialError::ImportError)?;

    if packets.is_empty() {
        return Err(SerialError::NoRecordedData);
    }

    let channel_count = packets.first().map(|item| item.data.len()).unwrap_or(0);
    let frame_count = packets.len();
    let frames = packets
        .into_iter()
        .map(|packet| SerialImportFrame {
            data: packet.data,
            dts_ms: packet.dts_ms,
        })
        .collect();

    Ok(SerialImportResponse {
        file_name,
        frame_count,
        channel_count,
        frames,
        message: "imported".to_string(),
    })
}

#[tauri::command]
pub fn serial_import_csv_from_path(file_path: String) -> Result<SerialImportResponse, SerialError> {
    let path = resolve_import_path(file_path)?;
    let file_name = path
        .file_name()
        .and_then(|value| value.to_str())
        .map(ToOwned::to_owned)
        .unwrap_or_else(|| "import.csv".to_string());

    let bytes = std::fs::read(&path).map_err(|_| SerialError::ImportError)?;
    let csv_content = String::from_utf8_lossy(&bytes).to_string();
    serial_import_csv(file_name, csv_content)
}

fn resolve_record_for_export(
    state: &State<'_, SerialConnectionState>,
) -> Result<SharedTactileRecording, SerialError> {
    let current_record = {
        let session = state.session.lock().map_err(|_| SerialError::StateError)?;
        session
            .as_ref()
            .map(|current_session| current_session.current_record.clone())
    };

    if let Some(recording) = current_record {
        return Ok(recording);
    }

    let last_record = state.last_record.lock().map_err(|_| SerialError::StateError)?;
    last_record.clone().ok_or(SerialError::NoRecordedData)
}

fn snapshot_record_frame_count(
    state: &State<'_, SerialConnectionState>,
) -> Result<usize, SerialError> {
    let current_record = {
        let session = state.session.lock().map_err(|_| SerialError::StateError)?;
        session
            .as_ref()
            .map(|current_session| current_session.current_record.clone())
    };

    if let Some(record) = current_record {
        return record
            .lock()
            .map(|recording| recording.frames.len())
            .map_err(|_| SerialError::StateError);
    }

    let last_record = state.last_record.lock().map_err(|_| SerialError::StateError)?;
    let Some(record) = last_record.as_ref() else {
        return Ok(0);
    };

    record
        .lock()
        .map(|recording| recording.frames.len())
        .map_err(|_| SerialError::StateError)
}

fn write_record_to_csv(
    record: SharedTactileRecording,
    output_path: &Path,
) -> Result<usize, SerialError> {
    if let Some(parent) = output_path.parent() {
        if !parent.exists() {
            return Err(SerialError::ExportError);
        }
    }

    let mut file = File::create(output_path).map_err(|_| SerialError::ExportError)?;
    let frame_count = {
        let recording = record.lock().map_err(|_| SerialError::StateError)?;
        if recording.frames.is_empty() {
            return Err(SerialError::NoRecordedData);
        }

        export_recording_csv(&recording, &mut file).map_err(|_| SerialError::ExportError)?;
        recording.frames.len()
    };

    Ok(frame_count)
}

fn resolve_export_path(raw_path: String) -> Result<PathBuf, SerialError> {
    let trimmed = raw_path.trim();
    if trimmed.is_empty() {
        return Err(SerialError::ExportError);
    }

    let mut path = resolve_absolute_path(trimmed).map_err(|_| SerialError::ExportError)?;
    if path.extension().is_none() {
        path.set_extension("csv");
    }

    if path.file_name().is_none() {
        return Err(SerialError::ExportError);
    }

    Ok(path)
}

fn resolve_import_path(raw_path: String) -> Result<PathBuf, SerialError> {
    let trimmed = raw_path.trim();
    if trimmed.is_empty() {
        return Err(SerialError::ImportError);
    }

    let path = resolve_absolute_path(trimmed).map_err(|_| SerialError::ImportError)?;
    if !path.exists() || !path.is_file() {
        return Err(SerialError::ImportError);
    }

    Ok(path)
}

fn resolve_absolute_path(raw_path: &str) -> std::io::Result<PathBuf> {
    let path = PathBuf::from(raw_path);
    if path.is_absolute() {
        Ok(path)
    } else {
        Ok(std::env::current_dir()?.join(path))
    }
}
