use chrono::{Local, TimeZone};
use cosmic::iced::advanced::image::{self, Renderer as ImageRenderer};
use cosmic::iced::advanced::layout::{self, Layout};
use cosmic::iced::advanced::renderer;
use cosmic::iced::advanced::text;
use cosmic::iced::advanced::text::{Paragraph as _, Renderer as _};
use cosmic::iced::advanced::widget::{self, Widget};
use cosmic::iced::advanced::{Clipboard, Renderer as _, Shell};
use cosmic::iced::border;
use cosmic::iced::mouse;
use cosmic::iced::{
    Background, Color, Event, Length, Pixels, Point, Rectangle, Size, Vector, alignment,
};
use cosmic::{Element, Renderer, Theme};
use regex::Regex;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use std::ops::Range;
use std::sync::OnceLock;
use std::time::Instant;
use tracing::Level;

const BUBBLE_MARGIN: f32 = 24.0;
const MAX_BUBBLE_WIDTH: f32 = 0.74;
const IMAGE_WIDTH_FACTOR: f32 = 0.70;
const RADIUS: f32 = 16.0;
const TEXT_X_PADDING: f32 = 14.0;
const TEXT_Y_PADDING: f32 = 8.0;
const DATE_PADDING: f32 = 36.0;
const SENDER_PADDING: f32 = 6.0;
const TRANSCRIPT_TOP_PADDING: f32 = 10.0;

type Paragraph = <Renderer as text::Renderer>::Paragraph;

#[derive(Clone, Debug, Hash)]
pub struct TranscriptMessage {
    pub id: String,
    pub sender: String,
    pub text: String,
    pub date_unix: i64,
    pub from_me: bool,
    pub should_animate: bool,
    pub attachments: Vec<TranscriptAttachment>,
}

#[derive(Clone, Debug, Hash)]
pub struct TranscriptAttachment {
    pub guid: String,
    pub preview_path: Option<String>,
    pub downloaded: bool,
    pub preview_downloaded: bool,
    pub width: Option<u32>,
    pub height: Option<u32>,
}

#[derive(Clone, Debug)]
pub enum TranscriptAction {
    CopyText(String),
    OpenUrl(String),
    OpenAttachment(String),
}

pub struct Transcript<'a, Message> {
    messages: Vec<TranscriptMessage>,
    show_sender: bool,
    fingerprint: u64,
    on_action: Box<dyn Fn(TranscriptAction) -> Message + 'a>,
}

impl<'a, Message> Transcript<'a, Message> {
    pub fn new(
        messages: Vec<TranscriptMessage>,
        show_sender: bool,
        on_action: impl Fn(TranscriptAction) -> Message + 'a,
    ) -> Self {
        let fingerprint = messages_fingerprint(&messages);
        Self {
            messages,
            show_sender,
            fingerprint,
            on_action: Box::new(on_action),
        }
    }
}

fn messages_fingerprint(messages: &[TranscriptMessage]) -> u64 {
    let mut hasher = DefaultHasher::new();
    messages.hash(&mut hasher);
    hasher.finish()
}

#[derive(Clone)]
enum DisplayKind {
    Date {
        paragraph: Paragraph,
    },
    Sender {
        paragraph: Paragraph,
    },
    Text {
        paragraph: Paragraph,
        text: String,
        from_me: bool,
    },
    Image {
        attachment_guid: String,
        preview_handle: Option<image::Handle>,
        from_me: bool,
        downloaded: bool,
    },
}

#[derive(Clone)]
struct DisplayItem {
    bounds: Rectangle,
    content_bounds: Rectangle,
    kind: DisplayKind,
}

#[derive(Default)]
struct DisplayList {
    items: Vec<DisplayItem>,
    height: f32,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct DisplayListKey {
    width_px: u32,
    show_sender: bool,
    fingerprint: u64,
}

#[derive(Default)]
struct TranscriptState {
    cache_key: Option<DisplayListKey>,
    display_list: DisplayList,
}

impl DisplayList {
    fn hit(&self, point: Point) -> Option<&DisplayItem> {
        let start = self
            .items
            .partition_point(|item| item.bounds.y + item.bounds.height < point.y);

        self.items[start..]
            .iter()
            .take_while(|item| item.bounds.y <= point.y)
            .find(|item| item.bounds.contains(point))
    }

    fn visible_range(&self, viewport: Rectangle) -> Range<usize> {
        let top = viewport.y;
        let bottom = viewport.y + viewport.height;
        let start = self
            .items
            .partition_point(|item| item.bounds.y + item.bounds.height < top);
        let end = start
            + self.items[start..]
                .iter()
                .take_while(|item| item.bounds.y <= bottom)
                .count();

        start..end
    }
}

fn url_regex() -> &'static Regex {
    static URL_RE: OnceLock<Regex> = OnceLock::new();
    URL_RE.get_or_init(|| {
        Regex::new(r"https?://(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-z]{2,6}\b[-a-zA-Z0-9@:%_+.~#?&//=]*")
            .expect("valid URL regex")
    })
}

fn first_url(text: &str) -> Option<String> {
    url_regex().find(text).map(|m| m.as_str().to_string())
}

fn is_attachment_marker(text: &str) -> bool {
    text == "\u{FFFC}"
}

fn date_label(timestamp: i64) -> String {
    Local
        .timestamp_opt(timestamp, 0)
        .single()
        .map(|dt| dt.format("%b %d, %Y at %H:%M").to_string())
        .unwrap_or_else(|| "Unknown date".to_string())
}

fn paragraph(
    renderer: &Renderer,
    content: &str,
    width: f32,
    size: f32,
    line_height: f32,
    align_x: text::Alignment,
) -> Paragraph {
    Paragraph::with_text(text::Text {
        content,
        bounds: Size::new(width, f32::INFINITY),
        size: Pixels(size),
        line_height: text::LineHeight::Relative(line_height),
        font: renderer.default_font(),
        align_x,
        align_y: alignment::Vertical::Top,
        shaping: text::Shaping::Advanced,
        wrapping: text::Wrapping::WordOrGlyph,
        ellipsize: text::Ellipsize::None,
    })
}

fn build_display_list(
    messages: &[TranscriptMessage],
    show_sender: bool,
    width: f32,
    renderer: &Renderer,
) -> DisplayList {
    let max_width = (width * MAX_BUBBLE_WIDTH).max(220.0);
    let max_image_width = max_width * IMAGE_WIDTH_FACTOR;
    let mut items = Vec::new();
    let mut y = TRANSCRIPT_TOP_PADDING;
    let mut last_date: Option<i64> = None;
    let mut last_sender: Option<&str> = None;

    for message in messages {
        if last_date.is_some_and(|date| message.date_unix - date > 60 * 60) {
            let text = date_label(message.date_unix);
            let paragraph = paragraph(
                renderer,
                &text,
                max_width,
                12.0,
                1.2,
                text::Alignment::Center,
            );
            let height = paragraph.min_height() + DATE_PADDING;
            let x = ((width - max_width) / 2.0).max(BUBBLE_MARGIN);
            let bounds = Rectangle::new(Point::new(x, y), Size::new(max_width, height));
            items.push(DisplayItem {
                bounds,
                content_bounds: bounds,
                kind: DisplayKind::Date { paragraph },
            });
            y += height;
            last_sender = None;
        }

        if show_sender && !message.from_me && last_sender != Some(message.sender.as_str()) {
            let paragraph = paragraph(
                renderer,
                &message.sender,
                max_width,
                12.0,
                1.2,
                text::Alignment::Left,
            );
            let height = paragraph.min_height() + SENDER_PADDING;
            let x = BUBBLE_MARGIN;
            let bounds = Rectangle::new(Point::new(x, y), Size::new(max_width, height));
            items.push(DisplayItem {
                bounds,
                content_bounds: bounds,
                kind: DisplayKind::Sender { paragraph },
            });
            y += height;
        }

        if !message.text.is_empty() && !is_attachment_marker(&message.text) {
            let text_available_width = max_width - TEXT_X_PADDING * 2.0;
            let paragraph = paragraph(
                renderer,
                &message.text,
                text_available_width,
                14.0,
                1.18,
                text::Alignment::Left,
            );
            let text_bounds = paragraph.min_bounds();
            let bubble_width = (text_bounds.width + TEXT_X_PADDING * 2.0).min(max_width);
            let bubble_height = text_bounds.height + TEXT_Y_PADDING * 2.0;
            let x = if message.from_me {
                width - bubble_width - BUBBLE_MARGIN
            } else {
                BUBBLE_MARGIN
            };
            let vertical_padding = if last_sender == Some(message.sender.as_str()) {
                4.0
            } else {
                10.0
            };
            let bounds = Rectangle::new(Point::new(x, y), Size::new(bubble_width, bubble_height));
            let content_bounds = Rectangle::new(
                Point::new(x + TEXT_X_PADDING, y + TEXT_Y_PADDING),
                Size::new(text_available_width, text_bounds.height),
            );
            items.push(DisplayItem {
                bounds,
                content_bounds,
                kind: DisplayKind::Text {
                    paragraph,
                    text: message.text.clone(),
                    from_me: message.from_me,
                },
            });
            y += bubble_height + vertical_padding;
        }

        for attachment in &message.attachments {
            let intrinsic_width = attachment.width.unwrap_or(200) as f32;
            let intrinsic_height = attachment.height.unwrap_or(150) as f32;
            let scale = (max_image_width / intrinsic_width).min(1.0);
            let image_width = (intrinsic_width * scale).max(200.0).min(max_image_width);
            let image_height = (intrinsic_height * scale).max(100.0);
            let x = if message.from_me {
                width - image_width - BUBBLE_MARGIN
            } else {
                BUBBLE_MARGIN
            };
            let bounds = Rectangle::new(Point::new(x, y), Size::new(image_width, image_height));
            items.push(DisplayItem {
                bounds,
                content_bounds: bounds,
                kind: DisplayKind::Image {
                    attachment_guid: attachment.guid.clone(),
                    preview_handle: attachment
                        .preview_path
                        .as_ref()
                        .map(image::Handle::from_path),
                    from_me: message.from_me,
                    downloaded: attachment.downloaded || attachment.preview_downloaded,
                },
            });
            y += image_height + 8.0;
        }

        last_sender = Some(&message.sender);
        last_date = Some(message.date_unix);
    }

    DisplayList {
        items,
        height: y.max(200.0),
    }
}

fn incoming_color() -> Color {
    Color::from_rgba(1.0, 1.0, 1.0, 0.08)
}

fn outgoing_color(theme: &Theme) -> Color {
    theme.cosmic().accent.base.into()
}

fn draw_bubble_background(
    renderer: &mut Renderer,
    bounds: Rectangle,
    _from_me: bool,
    color_value: Color,
) {
    renderer.fill_quad(
        renderer::Quad {
            bounds,
            border: border::rounded(RADIUS),
            ..renderer::Quad::default()
        },
        Background::Color(color_value),
    );
}

fn draw_placeholder(renderer: &mut Renderer, bounds: Rectangle, downloaded: bool) {
    let color_value = if downloaded {
        Color::from_rgba(1.0, 1.0, 1.0, 0.14)
    } else {
        Color::from_rgba(1.0, 1.0, 1.0, 0.08)
    };
    renderer.fill_quad(
        renderer::Quad {
            bounds,
            border: border::rounded(RADIUS),
            ..renderer::Quad::default()
        },
        Background::Color(color_value),
    );
}

impl<Message> Widget<Message, Theme, Renderer> for Transcript<'_, Message>
where
    Message: Clone,
{
    fn tag(&self) -> widget::tree::Tag {
        widget::tree::Tag::of::<TranscriptState>()
    }

    fn state(&self) -> widget::tree::State {
        widget::tree::State::new(TranscriptState::default())
    }

    fn size(&self) -> Size<Length> {
        Size {
            width: Length::Fill,
            height: Length::Shrink,
        }
    }

    fn layout(
        &mut self,
        tree: &mut widget::Tree,
        renderer: &Renderer,
        limits: &layout::Limits,
    ) -> layout::Node {
        let width = limits.max().width.max(320.0);
        let state = tree.state.downcast_mut::<TranscriptState>();
        let key = DisplayListKey {
            width_px: width.round() as u32,
            show_sender: self.show_sender,
            fingerprint: self.fingerprint,
        };
        if state.cache_key != Some(key) {
            let trace_start =
                tracing::enabled!(target: "kordophone_cosmic::transcript", Level::TRACE)
                    .then(Instant::now);
            state.display_list =
                build_display_list(&self.messages, self.show_sender, width, renderer);
            state.cache_key = Some(key);
            if let Some(start) = trace_start {
                tracing::trace!(
                    target: "kordophone_cosmic::transcript",
                    elapsed_us = start.elapsed().as_micros(),
                    messages = self.messages.len(),
                    items = state.display_list.items.len(),
                    width_px = key.width_px,
                    "rebuilt transcript display list"
                );
            }
        }
        layout::Node::new(Size::new(width, state.display_list.height))
    }

    fn update(
        &mut self,
        tree: &mut widget::Tree,
        event: &Event,
        layout: Layout<'_>,
        cursor: mouse::Cursor,
        renderer: &Renderer,
        _clipboard: &mut dyn Clipboard,
        shell: &mut Shell<'_, Message>,
        _viewport: &Rectangle,
    ) {
        let Event::Mouse(mouse::Event::ButtonPressed(button)) = event else {
            return;
        };

        let Some(position) = cursor.position_in(layout.bounds()) else {
            return;
        };

        let state = tree.state.downcast_mut::<TranscriptState>();
        if state.display_list.items.is_empty() {
            state.display_list = build_display_list(
                &self.messages,
                self.show_sender,
                layout.bounds().width,
                renderer,
            );
        }
        let display_list = &state.display_list;
        let point = Point::new(position.x, position.y);
        let Some(item) = display_list.hit(point) else {
            return;
        };

        let action = match (&item.kind, button) {
            (DisplayKind::Text { text, .. }, mouse::Button::Right) => {
                Some(TranscriptAction::CopyText(text.clone()))
            }
            (DisplayKind::Text { text, .. }, mouse::Button::Left) => {
                first_url(text).map(TranscriptAction::OpenUrl)
            }
            (
                DisplayKind::Image {
                    attachment_guid, ..
                },
                mouse::Button::Left,
            ) => Some(TranscriptAction::OpenAttachment(attachment_guid.clone())),
            _ => None,
        };

        if let Some(action) = action {
            shell.publish((self.on_action)(action));
            shell.capture_event();
        }
    }

    fn draw(
        &self,
        tree: &widget::Tree,
        renderer: &mut Renderer,
        theme: &Theme,
        style: &renderer::Style,
        layout: Layout<'_>,
        _cursor: mouse::Cursor,
        viewport: &Rectangle,
    ) {
        let display_list = &tree.state.downcast_ref::<TranscriptState>().display_list;
        let trace_start = tracing::enabled!(target: "kordophone_cosmic::transcript", Level::TRACE)
            .then(Instant::now);
        let offset = Vector::new(layout.bounds().x, layout.bounds().y);
        let local_viewport = Rectangle::new(
            Point::new(viewport.x - offset.x, viewport.y - offset.y),
            viewport.size(),
        );
        let visible_range = display_list.visible_range(local_viewport);
        let visible_count = visible_range.len();
        renderer.with_translation(offset, |renderer| {
            for item in &display_list.items[visible_range] {
                let item_rect =
                    Rectangle::new(Point::new(item.bounds.x, item.bounds.y), item.bounds.size());

                match &item.kind {
                    DisplayKind::Date { paragraph } => {
                        renderer.fill_paragraph(
                            paragraph,
                            Point::new(item_rect.x, item_rect.y + DATE_PADDING / 2.0),
                            Color::from_rgba(1.0, 1.0, 1.0, 0.48),
                            local_viewport,
                        );
                    }
                    DisplayKind::Sender { paragraph } => {
                        renderer.fill_paragraph(
                            paragraph,
                            Point::new(item_rect.x + TEXT_X_PADDING, item_rect.y + SENDER_PADDING),
                            Color::from_rgba(1.0, 1.0, 1.0, 0.78),
                            local_viewport,
                        );
                    }
                    DisplayKind::Text {
                        paragraph, from_me, ..
                    } => {
                        let bubble_color = if *from_me {
                            outgoing_color(theme)
                        } else {
                            incoming_color()
                        };
                        draw_bubble_background(renderer, item_rect, *from_me, bubble_color);
                        renderer.fill_paragraph(
                            paragraph,
                            Point::new(item.content_bounds.x, item.content_bounds.y),
                            if *from_me {
                                theme.cosmic().accent.on.into()
                            } else {
                                style.text_color
                            },
                            local_viewport,
                        );
                    }
                    DisplayKind::Image {
                        preview_handle,
                        from_me,
                        downloaded,
                        ..
                    } => {
                        let bubble_color = if *from_me {
                            outgoing_color(theme)
                        } else {
                            incoming_color()
                        };
                        draw_bubble_background(renderer, item_rect, *from_me, bubble_color);
                        if let Some(handle) = preview_handle {
                            renderer.draw_image(
                                image::Image {
                                    handle: handle.clone(),
                                    border_radius: border::Radius::from(RADIUS),
                                    filter_method: image::FilterMethod::Linear,
                                    rotation: cosmic::iced::Radians(0.0),
                                    opacity: 1.0,
                                    snap: true,
                                },
                                item_rect,
                                item_rect,
                            );
                        } else {
                            draw_placeholder(renderer, item_rect, *downloaded);
                        }
                    }
                }
            }
        });
        if let Some(start) = trace_start {
            tracing::trace!(
                target: "kordophone_cosmic::transcript",
                elapsed_us = start.elapsed().as_micros(),
                visible_items = visible_count,
                total_items = display_list.items.len(),
                viewport_y = local_viewport.y,
                viewport_height = local_viewport.height,
                "drew transcript visible range"
            );
        }
    }

    fn mouse_interaction(
        &self,
        tree: &widget::Tree,
        layout: Layout<'_>,
        cursor: mouse::Cursor,
        _viewport: &Rectangle,
        _renderer: &Renderer,
    ) -> mouse::Interaction {
        let Some(position) = cursor.position_in(layout.bounds()) else {
            return mouse::Interaction::default();
        };
        let display_list = &tree.state.downcast_ref::<TranscriptState>().display_list;
        if display_list
            .hit(Point::new(position.x, position.y))
            .is_some()
        {
            mouse::Interaction::Pointer
        } else {
            mouse::Interaction::default()
        }
    }
}

impl<'a, Message> From<Transcript<'a, Message>> for Element<'a, Message>
where
    Message: Clone + 'a,
{
    fn from(value: Transcript<'a, Message>) -> Self {
        Element::new(value)
    }
}
