use std::fmt;

use busylib::proto::bsb_error::{Cause, Severity};
use busylib::proto::bsb_state::State;
use busylib::proto::bsb_state::state_update::State as Update;
use serde::Serialize;

use crate::reporter::events::CliEvent;
use crate::reporter::events::binary::Payload;
use crate::types::frame::{encoding_name, format_name, screen_name};

#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct StreamingScreenEvent(Payload);

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub struct StreamError {
    cause: &'static str,
    severity: &'static str,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct Labelled {
    name: &'static str,
    value: i32,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct FramePayload {
    screen: Labelled,
    width: u32,
    height: u32,
    encoding: Labelled,
    pixel_format: Labelled,
    bytes: usize,
    #[serde(skip_serializing_if = "Option::is_none")]
    image_format: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    base64: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    path: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    reason: Option<String>,
}

impl FramePayload {
    pub fn new(
        frame: &busylib::proto::bsb_frame::Frame,
        image: Option<(String, String)>,
        path: Option<String>,
        reason: Option<String>,
    ) -> Self {
        let (image_format, base64) = match image {
            Some((format, encoded)) => (Some(format), Some(encoded)),
            None => (None, None),
        };

        Self {
            screen: Labelled {
                name: screen_name(frame.screen()),
                value: frame.screen,
            },
            width: frame.width,
            height: frame.height,
            encoding: Labelled {
                name: encoding_name(frame.encoding()),
                value: frame.encoding,
            },
            pixel_format: Labelled {
                name: format_name(frame.pixel_format()),
                value: frame.pixel_format,
            },
            bytes: frame.data.len(),
            image_format,
            base64,
            path,
            reason,
        }
    }
}

#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct StreamingStatusEvent {
    sequence: u64,
    timestamp: u64,
    #[serde(skip_serializing_if = "Option::is_none")]
    frame: Option<FramePayload>,
    #[serde(skip_serializing_if = "Option::is_none")]
    update: Option<Update>,
    #[serde(skip_serializing_if = "Option::is_none")]
    error: Option<StreamError>,
}

impl StreamingStatusEvent {
    pub fn error_of(state: &State) -> Option<StreamError> {
        state.error.as_ref().map(|error| StreamError {
            cause: cause_label(error.cause()),
            severity: severity_label(error.severity()),
        })
    }

    pub fn heartbeat(sequence: u64, timestamp: u64, error: Option<StreamError>) -> Self {
        Self {
            sequence,
            timestamp,
            frame: None,
            update: None,
            error,
        }
    }

    pub fn frame(
        sequence: u64,
        timestamp: u64,
        error: Option<StreamError>,
        frame: FramePayload,
    ) -> Self {
        Self {
            sequence,
            timestamp,
            frame: Some(frame),
            update: None,
            error,
        }
    }

    pub fn update(
        sequence: u64,
        timestamp: u64,
        error: Option<StreamError>,
        update: Update,
    ) -> Self {
        Self {
            sequence,
            timestamp,
            frame: None,
            update: Some(update),
            error,
        }
    }
}

impl From<StreamingStatusEvent> for CliEvent {
    fn from(event: StreamingStatusEvent) -> Self {
        CliEvent::StreamingStatus(Box::new(event))
    }
}

impl fmt::Display for StreamingStatusEvent {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "#{} ", self.sequence)?;

        match (&self.frame, &self.update) {
            (Some(frame), _) => write_frame(f, frame)?,
            (None, Some(update)) => write_update(f, update)?,
            (None, None) => f.write_str("heartbeat")?,
        }

        match &self.error {
            Some(StreamError { cause, severity }) => write!(f, " ({severity}: {cause})"),
            None => Ok(()),
        }
    }
}

fn write_frame(f: &mut fmt::Formatter<'_>, frame: &FramePayload) -> fmt::Result {
    write!(
        f,
        "frame {} {}x{} {} {}",
        frame.screen.name, frame.width, frame.height, frame.encoding.name, frame.pixel_format.name
    )?;

    match (&frame.path, &frame.reason) {
        (Some(path), _) => write!(f, " -> {path}"),
        (None, Some(reason)) => write!(f, " ({reason})"),
        (None, None) => Ok(()),
    }
}

fn write_update(f: &mut fmt::Formatter<'_>, update: &Update) -> fmt::Result {
    match update {
        Update::Brightness(brightness) => {
            write!(f, "brightness {}", brightness.actual_brightness)
        }
        Update::AudioVolume(volume) => write!(f, "audio volume {}", volume.volume),
        Update::DeviceName(name) => write!(f, "device name {}", name.name),
        other => write!(f, "{}", update_label(other)),
    }
}

fn update_label(update: &Update) -> &'static str {
    match update {
        Update::DeviceName(_) => "device name",
        Update::Power(_) => "power",
        Update::Brightness(_) => "brightness",
        Update::AudioVolume(_) => "audio volume",
        Update::Wifi(_) => "wifi",
        Update::UpdateState(_) => "update state",
        Update::UpdateCheck(_) => "update check",
        Update::Timezone(_) => "timezone",
        Update::Matter(_) => "matter",
        Update::Frame(_) => "frame",
        Update::Input(_) => "input",
        Update::Timer(_) => "timer",
        Update::Ble(_) => "ble",
        Update::AutoUpdateState(_) => "auto update state",
        Update::TimerProfiles(_) => "timer profiles",
    }
}

fn cause_label(cause: Cause) -> &'static str {
    match cause {
        Cause::ResourceLimit => "resource limit",
    }
}

fn severity_label(severity: Severity) -> &'static str {
    match severity {
        Severity::Fatal => "fatal",
        Severity::Error => "error",
        Severity::Warning => "warning",
    }
}

impl StreamingScreenEvent {
    pub fn new(payload: Payload) -> Self {
        Self(payload)
    }
}

impl From<StreamingScreenEvent> for CliEvent {
    fn from(event: StreamingScreenEvent) -> Self {
        CliEvent::StreamingScreen(event)
    }
}

impl fmt::Display for StreamingScreenEvent {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        self.0.fmt(f)
    }
}

#[cfg(test)]
mod tests {
    use base64::Engine as _;
    use base64::engine::general_purpose::STANDARD;
    use busylib::proto::bsb_frame::{Encoding, Frame, PixelFormat, Screen};
    use busylib::proto::bsb_state::Brightness;
    use serde_json::json;

    use super::*;

    fn frame() -> Frame {
        Frame {
            screen: Screen::Front as i32,
            width: 72,
            height: 16,
            encoding: Encoding::RunLength as i32,
            pixel_format: PixelFormat::Rgb888 as i32,
            data: vec![0x7f, 0, 0, 0],
        }
    }

    #[test]
    fn numbers_each_message_it_reports() {
        let event = StreamingStatusEvent::heartbeat(7, 1785692728867, None);

        assert_eq!(event.to_string(), "#7 heartbeat");
        assert_eq!(
            serde_json::to_value(CliEvent::from(event)).unwrap(),
            json!({"event": "streaming_status", "sequence": 7, "timestamp": 1785692728867u64})
        );
    }

    #[test]
    fn reports_a_frame_with_both_label_and_wire_value() {
        let payload = FramePayload::new(
            &frame(),
            Some(("bmp".to_owned(), STANDARD.encode(b"image"))),
            None,
            None,
        );
        let event = StreamingStatusEvent::frame(1, 1785692728867, None, payload);

        assert_eq!(event.to_string(), "#1 frame front 72x16 run-length rgb888");

        let json = serde_json::to_value(CliEvent::from(event)).unwrap();
        let frame = &json["frame"];

        assert_eq!(frame["screen"], json!({"name": "front", "value": 0}));
        assert_eq!(frame["encoding"], json!({"name": "run-length", "value": 1}));
        assert_eq!(frame["pixel_format"], json!({"name": "rgb888", "value": 0}));
        assert_eq!(frame["bytes"], 4);
        assert_eq!(frame["image_format"], "bmp");
        assert_eq!(frame["base64"], "aW1hZ2U=");
        assert!(frame.get("path").is_none());
    }

    #[test]
    fn reports_where_a_frame_was_written() {
        let payload = FramePayload::new(
            &frame(),
            Some(("png".to_owned(), STANDARD.encode(b"image"))),
            Some("frames/front-000001.png".to_owned()),
            None,
        );
        let event = StreamingStatusEvent::frame(1, 1785692728867, None, payload);

        assert_eq!(
            event.to_string(),
            "#1 frame front 72x16 run-length rgb888 -> frames/front-000001.png"
        );
    }

    #[test]
    fn leaves_out_the_image_when_it_is_not_reported() {
        let payload = FramePayload::new(&frame(), None, None, None);
        let event = StreamingStatusEvent::frame(1, 1785692728867, None, payload);

        let json = serde_json::to_value(CliEvent::from(event)).unwrap();
        let frame = &json["frame"];

        assert!(frame.get("base64").is_none());
        assert!(frame.get("image_format").is_none());
        assert_eq!(frame["bytes"], 4);
    }

    #[test]
    fn falls_back_to_the_raw_payload_and_says_why() {
        let payload = FramePayload::new(
            &frame(),
            Some(("raw".to_owned(), STANDARD.encode(b"device"))),
            None,
            Some("the device streamed a deflate frame, which is not supported yet".to_owned()),
        );
        let event = StreamingStatusEvent::frame(2, 1785692728867, None, payload);

        assert_eq!(
            event.to_string(),
            "#2 frame front 72x16 run-length rgb888 \
             (the device streamed a deflate frame, which is not supported yet)"
        );

        let json = serde_json::to_value(CliEvent::from(event)).unwrap();

        assert_eq!(json["frame"]["image_format"], "raw");
        assert_eq!(json["frame"]["base64"], "ZGV2aWNl");
        assert!(
            json["frame"]["reason"]
                .as_str()
                .unwrap()
                .contains("deflate")
        );
        assert!(json["frame"].get("path").is_none());
    }

    #[test]
    fn reports_other_updates_by_their_value() {
        let update = Update::Brightness(Brightness {
            setting: None,
            actual_brightness: 30,
        });
        let event = StreamingStatusEvent::update(3, 1785692728867, None, update);

        assert_eq!(event.to_string(), "#3 brightness 30");
    }

    #[test]
    fn reports_a_frame_as_a_file_or_as_base64() {
        let written = StreamingScreenEvent::new(Payload::written(b"AAAA", "front.txt".to_string()));

        assert_eq!(written.to_string(), "wrote 4 bytes to front.txt");
        assert_eq!(
            serde_json::to_value(CliEvent::from(written)).unwrap(),
            json!({"event": "streaming_screen", "bytes": 4, "path": "front.txt"})
        );
    }
}