use std::sync::Arc;
use std::time::Duration;

use crate::frb_generated::StreamSink;
use flutter_rust_bridge::frb;
use kusoft_net::{
    auth_fingerprint, command, decode_tagged_json, encode_tagged_json,
    set_trust_mincifry_ca as set_core_trust, trust_mincifry_ca as core_trust,
    CallSignaling as CoreCallSignaling, Direction, ProxyConfig, Session,
    SessionOptions as CoreSessionOptions, SessionState, WireEvent,
};
use tokio::runtime::Runtime;

pub struct SessionOptions {
    pub host: String,
    pub port: u16,
    pub device_id: String,
    pub instance_id: String,
    pub app_version: String,
    pub build_number: i64,
    pub device_type: String,
    pub os_version: String,
    pub timezone: String,
    pub screen: String,
    pub push_device_type: String,
    pub arch: String,
    pub locale: String,
    pub device_name: String,
    pub device_locale: String,
    pub client_session_id: i64,
    pub ping_interval_secs: u64,
    pub ping_interactive: bool,
    pub auto_reconnect: bool,
    pub insecure_tls: bool,
    pub proxy: Option<String>,
    pub is_pwa: Option<bool>,
    pub header_user_agent: Option<String>,
}

pub struct HandshakeInfo {
    pub calls_seed: Option<i64>,
    pub device_name: Option<String>,
    pub payload: Vec<u8>,
    pub payload_json: String,
}

pub struct PushEvent {
    pub opcode: u16,
    pub payload: Vec<u8>,
    pub payload_json: String,
}

pub struct RequestOutcome {
    pub cmd: u8,
    pub opcode: u16,
    pub payload_json: String,
    pub error_message: Option<String>,
    pub error_key: Option<String>,
}

pub struct WireLogEvent {
    pub direction: String,
    pub cmd: String,
    pub opcode: u16,
    pub seq: u16,
    pub json: String,
}

pub enum UploadEvent {
    Progress { sent: u64, total: u64 },
    Done { status: u16, body: Vec<u8> },
    Error { message: String },
}

pub struct CallParams {
    pub token: String,
    pub ws_endpoint: String,
    pub stun: Option<String>,
    pub turn: Vec<String>,
    pub turn_user: Option<String>,
    pub turn_password: Option<String>,
    pub is_video: bool,
}

pub struct CallSignaling {
    runtime: Arc<Runtime>,
    inner: Arc<CoreCallSignaling>,
}

impl CallSignaling {
    pub fn send_command(&self, command: String, extra_json: String) -> Result<String, String> {
        let extra: serde_json::Value =
            serde_json::from_str(&extra_json).map_err(|error| error.to_string())?;
        self.runtime
            .block_on(self.inner.send_command(&command, extra))
            .map(|value| value.to_string())
            .map_err(|error| error.to_string())
    }

    pub fn notifications(&self, sink: StreamSink<String>) {
        let mut notifications = self.inner.subscribe();
        self.runtime.spawn(async move {
            while let Ok(value) = notifications.recv().await {
                if sink.add(value.to_string()).is_err() {
                    break;
                }
            }
        });
    }

    #[frb(sync)]
    pub fn is_connected(&self) -> bool {
        self.inner.is_connected()
    }

    #[frb(sync)]
    pub fn close(&self) {
        self.inner.close();
    }
}

pub struct KusoftSession {
    runtime: Arc<Runtime>,
    inner: Arc<Session>,
    proxy: Option<ProxyConfig>,
    insecure_tls: bool,
    user_agent: String,
}

impl KusoftSession {
    #[frb(sync)]
    pub fn new(
        options: SessionOptions,
        wire_log: Option<StreamSink<WireLogEvent>>,
    ) -> Result<KusoftSession, String> {
        let runtime = Arc::new(
            tokio::runtime::Builder::new_multi_thread()
                .enable_all()
                .build()
                .map_err(|error| error.to_string())?,
        );
        let proxy = options
            .proxy
            .as_deref()
            .map(ProxyConfig::parse)
            .transpose()
            .map_err(|error| error.to_string())?;
        let insecure_tls = options.insecure_tls;
        let core_options = CoreSessionOptions {
            host: options.host,
            port: options.port,
            device_id: options.device_id,
            instance_id: options.instance_id,
            app_version: options.app_version,
            build_number: options.build_number,
            device_type: options.device_type,
            os_version: options.os_version,
            timezone: options.timezone,
            screen: options.screen,
            push_device_type: options.push_device_type,
            arch: options.arch,
            locale: options.locale,
            device_name: options.device_name,
            device_locale: options.device_locale,
            client_session_id: options.client_session_id,
            ping_interval: Duration::from_secs(options.ping_interval_secs),
            ping_interactive: options.ping_interactive,
            insecure_tls,
            proxy: options.proxy,
        };
        let user_agent = core_options.http_user_agent();
        let inner = Arc::new(Session::new(core_options));
        if let Some(sink) = wire_log {
            let mut events = inner.wire_events();
            runtime.spawn(async move {
                while let Ok(event) = events.recv().await {
                    if sink.add(wire_event(event)).is_err() {
                        break;
                    }
                }
            });
        }
        Ok(Self {
            runtime,
            inner,
            proxy,
            insecure_tls,
            user_agent,
        })
    }

    pub fn connect(&self) -> Result<HandshakeInfo, String> {
        let info = self
            .runtime
            .block_on(self.inner.connect())
            .map_err(|error| error.to_string())?;
        Ok(HandshakeInfo {
            calls_seed: info.calls_seed,
            device_name: info.device_name,
            payload: info.payload,
            payload_json: info.payload_json,
        })
    }

    pub fn request(&self, opcode: u16, payload: Vec<u8>) -> Result<Vec<u8>, String> {
        self.runtime
            .block_on(self.inner.request(opcode, &payload))
            .map(|packet| packet.payload)
            .map_err(|error| error.to_string())
    }

    pub fn request_json(&self, opcode: u16, json_in: String) -> Result<String, String> {
        let payload = encode_tagged_json(&json_in).map_err(|error| error.to_string())?;
        let packet = self
            .runtime
            .block_on(self.inner.request(opcode, &payload))
            .map_err(|error| error.to_string())?;
        decode_tagged_json(&packet.payload).map_err(|error| error.to_string())
    }

    pub fn request_full(&self, opcode: u16, json_in: String) -> Result<RequestOutcome, String> {
        let payload = encode_tagged_json(&json_in).map_err(|error| error.to_string())?;
        let packet = self
            .runtime
            .block_on(self.inner.request_raw(opcode, &payload))
            .map_err(|error| error.to_string())?;
        let payload_json =
            decode_tagged_json(&packet.payload).unwrap_or_else(|_| "null".to_string());
        let (error_message, error_key) = if packet.command == command::ERROR {
            error_fields(&payload_json)
        } else {
            (None, None)
        };
        Ok(RequestOutcome {
            cmd: packet.command,
            opcode: packet.opcode,
            payload_json,
            error_message,
            error_key,
        })
    }

    #[frb(sync)]
    pub fn send(&self, opcode: u16, payload: Vec<u8>) -> Result<u32, String> {
        self.inner
            .send(opcode, &payload)
            .map(u32::from)
            .map_err(|error| error.to_string())
    }

    pub fn pushes(&self, sink: StreamSink<PushEvent>) {
        let mut pushes = self.inner.pushes();
        self.runtime.spawn(async move {
            while let Ok(packet) = pushes.recv().await {
                let payload_json =
                    decode_tagged_json(&packet.payload).unwrap_or_else(|_| "null".to_string());
                if sink
                    .add(PushEvent {
                        opcode: packet.opcode,
                        payload: packet.payload,
                        payload_json,
                    })
                    .is_err()
                {
                    break;
                }
            }
        });
    }

    #[frb(sync)]
    pub fn set_ping_interactive(&self, interactive: bool) {
        self.inner.set_ping_interactive(interactive);
    }

    #[frb(sync)]
    pub fn ping_interactive(&self) -> bool {
        self.inner.ping_interactive()
    }

    #[frb(sync)]
    pub fn user_agent(&self) -> String {
        self.inner.user_agent()
    }

    #[frb(sync)]
    pub fn state(&self) -> String {
        match self.inner.state() {
            SessionState::Disconnected => "disconnected",
            SessionState::Connecting => "connecting",
            SessionState::Connected => "connected",
            SessionState::Online => "online",
        }
        .to_string()
    }

    #[frb(sync)]
    pub fn disconnect(&self) {
        self.inner.disconnect();
    }

    pub fn upload_file(
        &self,
        url: String,
        data: Vec<u8>,
        filename: String,
        user_agent: Option<String>,
        sink: StreamSink<UploadEvent>,
    ) {
        let proxy = self.proxy.clone();
        let insecure = self.insecure_tls;
        let user_agent = user_agent.unwrap_or_else(|| self.user_agent.clone());
        self.runtime
            .spawn(drive_upload(sink, move |progress| async move {
                kusoft_net::upload_file(
                    &url,
                    &data,
                    &filename,
                    insecure,
                    proxy.as_ref(),
                    &user_agent,
                    progress,
                )
                .await
                .map_err(|error| error.to_string())
            }));
    }

    pub fn upload_file_path(
        &self,
        url: String,
        path: String,
        filename: String,
        content_type: Option<String>,
        connection: Option<String>,
        user_agent: Option<String>,
        sink: StreamSink<UploadEvent>,
    ) {
        let proxy = self.proxy.clone();
        let insecure = self.insecure_tls;
        let user_agent = user_agent.unwrap_or_else(|| self.user_agent.clone());
        self.runtime
            .spawn(drive_upload(sink, move |progress| async move {
                kusoft_net::upload_file_path(
                    &url,
                    &path,
                    &filename,
                    content_type.as_deref(),
                    connection.as_deref(),
                    insecure,
                    proxy.as_ref(),
                    &user_agent,
                    progress,
                )
                .await
                .map_err(|error| error.to_string())
            }));
    }

    pub fn upload_photo(
        &self,
        url: String,
        data: Vec<u8>,
        filename: String,
        user_agent: Option<String>,
        sink: StreamSink<UploadEvent>,
    ) {
        let proxy = self.proxy.clone();
        let insecure = self.insecure_tls;
        let user_agent = user_agent.unwrap_or_else(|| self.user_agent.clone());
        self.runtime
            .spawn(drive_upload(sink, move |progress| async move {
                kusoft_net::upload_photo(
                    &url,
                    &data,
                    &filename,
                    insecure,
                    proxy.as_ref(),
                    &user_agent,
                    progress,
                )
                .await
                .map_err(|error| error.to_string())
            }));
    }

    pub fn upload_photo_path(
        &self,
        url: String,
        path: String,
        filename: String,
        user_agent: Option<String>,
        sink: StreamSink<UploadEvent>,
    ) {
        let proxy = self.proxy.clone();
        let insecure = self.insecure_tls;
        let user_agent = user_agent.unwrap_or_else(|| self.user_agent.clone());
        self.runtime
            .spawn(drive_upload(sink, move |progress| async move {
                kusoft_net::upload_photo_path(
                    &url,
                    &path,
                    &filename,
                    insecure,
                    proxy.as_ref(),
                    &user_agent,
                    progress,
                )
                .await
                .map_err(|error| error.to_string())
            }));
    }

    pub fn upload_video_path(
        &self,
        url: String,
        path: String,
        chunk_size: u32,
        concurrency: u32,
        sink: StreamSink<UploadEvent>,
    ) {
        let proxy = self.proxy.clone();
        let insecure = self.insecure_tls;
        self.runtime
            .spawn(drive_upload(sink, move |progress| async move {
                if concurrency == 0 {
                    return Err("concurrency must be positive".to_string());
                }
                kusoft_net::upload_video_path(
                    &url,
                    &path,
                    chunk_size as usize,
                    insecure,
                    proxy.as_ref(),
                    progress,
                )
                .await
                .map_err(|error| error.to_string())
            }));
    }
}

async fn drive_upload<F, Fut>(sink: StreamSink<UploadEvent>, upload: F)
where
    F: FnOnce(Arc<dyn Fn(u64, u64) + Send + Sync>) -> Fut,
    Fut: std::future::Future<Output = Result<kusoft_net::UploadResponse, String>>,
{
    let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
    let drain = tokio::spawn(async move {
        while let Some(event) = receiver.recv().await {
            if sink.add(event).is_err() {
                break;
            }
        }
    });
    let progress_sender = sender.clone();
    let progress: Arc<dyn Fn(u64, u64) + Send + Sync> = Arc::new(move |sent, total| {
        let _ = progress_sender.send(UploadEvent::Progress { sent, total });
    });
    let terminal = match upload(progress).await {
        Ok(response) => UploadEvent::Done {
            status: response.status,
            body: response.body,
        },
        Err(message) => UploadEvent::Error { message },
    };
    let _ = sender.send(terminal);
    drop(sender);
    let _ = drain.await;
}

#[frb(sync)]
pub fn decode_vcp(vcp: String, _conversation_id: String) -> Option<CallParams> {
    kusoft_net::decode_vcp(&vcp).map(|params| CallParams {
        token: params.token,
        ws_endpoint: params.ws_endpoint,
        stun: params.stun,
        turn: params.turn,
        turn_user: params.turn_user,
        turn_password: params.turn_password,
        is_video: params.is_video,
    })
}

pub fn connect_call_signaling(
    url: String,
    user_agent: Option<String>,
    proxy: Option<String>,
) -> Result<CallSignaling, String> {
    let runtime = Arc::new(
        tokio::runtime::Builder::new_multi_thread()
            .enable_all()
            .build()
            .map_err(|error| error.to_string())?,
    );
    let proxy = proxy
        .as_deref()
        .map(ProxyConfig::parse)
        .transpose()
        .map_err(|error| error.to_string())?;
    let inner = runtime
        .block_on(CoreCallSignaling::connect(
            &url,
            user_agent.as_deref().unwrap_or("okhttp/4.12.0"),
            proxy.as_ref(),
            false,
        ))
        .map_err(|error| error.to_string())?;
    Ok(CallSignaling {
        runtime,
        inner: Arc::new(inner),
    })
}

#[frb(sync)]
pub fn set_trust_mincifry_ca(enabled: bool) {
    set_core_trust(enabled);
}

#[frb(sync)]
pub fn trust_mincifry_ca() -> bool {
    core_trust()
}

#[frb(sync)]
pub fn auth_mode(
    signature: Vec<u8>,
    dex: Vec<u8>,
    so: Vec<u8>,
    calls_seed: i64,
    device_id: String,
) -> Vec<u8> {
    auth_fingerprint(&signature, &dex, &so, calls_seed, &device_id)
}

fn wire_event(event: WireEvent) -> WireLogEvent {
    let direction = match event.direction {
        Direction::Out => "out",
        Direction::In => "in",
    };
    let cmd = match event.packet.command {
        command::OK => "ok",
        command::NOT_FOUND => "not_found",
        command::ERROR => "error",
        _ if event.direction == Direction::Out => "request",
        _ => "push",
    };
    WireLogEvent {
        direction: direction.to_string(),
        cmd: cmd.to_string(),
        opcode: event.packet.opcode,
        seq: event.packet.sequence,
        json: decode_tagged_json(&event.packet.payload).unwrap_or_else(|_| "null".to_string()),
    }
}

fn error_fields(json: &str) -> (Option<String>, Option<String>) {
    let value: serde_json::Value = serde_json::from_str(json).unwrap_or(serde_json::Value::Null);
    let message = ["localizedMessage", "message", "title"]
        .iter()
        .find_map(|key| value.get(key).and_then(serde_json::Value::as_str))
        .map(str::to_string);
    let key = value
        .get("error")
        .and_then(serde_json::Value::as_str)
        .map(str::to_string);
    (message, key)
}
