use std::cell::RefCell;
use std::collections::VecDeque;
use std::io;
use std::rc::Rc;
use std::time::Duration;

use clap::Command;
use ratatui::Terminal;
use ratatui::backend::TestBackend;
use ratatui::layout::Rect;

use super::{DrawObserver, TerminalSession, event_loop_with_observer};
use crate::TuiConfig;
use crate::error::TuiError;
use crate::frame_snapshot::FrameSnapshot;
use crate::input::HoverTarget;
use crate::runtime::{
    AppEvent, AppKeyCode, AppKeyEvent, AppKeyModifiers, AppMouseButton, AppMouseEvent,
    AppMouseEventKind, Runtime,
};

#[derive(Debug, Clone)]
pub(crate) struct ObservedFrame {
    pub(crate) text: String,
    pub(crate) snapshot: FrameSnapshot,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum ScriptedOutcome {
    Run(Vec<String>),
    Cancelled,
}

#[derive(Debug, Clone)]
pub(crate) struct ScriptedRun {
    pub(crate) outcome: ScriptedOutcome,
    pub(crate) frames: Vec<ObservedFrame>,
}

#[derive(Debug, Clone)]
pub(crate) enum ScriptedStep {
    ExpectText(String),
    Event(AppEvent),
    TypeText(String),
    Paste(String),
    ClickFooter(HoverTarget),
    ClickInput(String),
    SelectDropdownOption(String),
    Resize { width: u16, height: u16 },
}

impl ScriptedStep {
    pub(crate) fn expect_text(text: impl Into<String>) -> Self {
        Self::ExpectText(text.into())
    }

    pub(crate) fn raw(event: AppEvent) -> Self {
        Self::Event(event)
    }

    pub(crate) fn key(code: AppKeyCode) -> Self {
        Self::Event(key_event(code))
    }

    pub(crate) fn ctrl(code: AppKeyCode) -> Self {
        Self::Event(ctrl_event(code))
    }

    pub(crate) fn type_text(text: impl Into<String>) -> Self {
        Self::TypeText(text.into())
    }

    pub(crate) fn paste(text: impl Into<String>) -> Self {
        Self::Paste(text.into())
    }

    pub(crate) fn click_footer(target: HoverTarget) -> Self {
        Self::ClickFooter(target)
    }

    pub(crate) fn click_input(arg_id: impl Into<String>) -> Self {
        Self::ClickInput(arg_id.into())
    }

    pub(crate) fn open_dropdown(arg_id: impl Into<String>) -> Self {
        Self::ClickInput(arg_id.into())
    }

    pub(crate) fn select_dropdown_option(label: impl Into<String>) -> Self {
        Self::SelectDropdownOption(label.into())
    }

    pub(crate) fn resize(width: u16, height: u16) -> Self {
        Self::Resize { width, height }
    }

    fn description(&self) -> String {
        match self {
            Self::ExpectText(text) => format!("text `{text}`"),
            Self::Event(event) => format!("raw event {event:?}"),
            Self::TypeText(text) => format!("typed text `{text}`"),
            Self::Paste(text) => format!("paste `{text}`"),
            Self::ClickFooter(target) => format!("footer target {target:?}"),
            Self::ClickInput(arg_id) => format!("input `{arg_id}`"),
            Self::SelectDropdownOption(label) => format!("dropdown option `{label}`"),
            Self::Resize { width, height } => format!("resize to {width}x{height}"),
        }
    }
}

#[derive(Debug, Clone)]
pub(crate) struct ScriptedHarness {
    command: Command,
    config: TuiConfig,
    width: u16,
    height: u16,
    steps: Vec<ScriptedStep>,
}

impl ScriptedHarness {
    pub(crate) fn new(command: Command) -> Self {
        Self {
            command,
            config: TuiConfig::default(),
            width: 80,
            height: 24,
            steps: Vec::new(),
        }
    }

    pub(crate) fn with_size(mut self, width: u16, height: u16) -> Self {
        self.width = width;
        self.height = height;
        self
    }

    pub(crate) fn with_config(mut self, config: TuiConfig) -> Self {
        self.config = config;
        self
    }

    pub(crate) fn script(mut self, steps: impl IntoIterator<Item = ScriptedStep>) -> Self {
        self.steps.extend(steps);
        self
    }

    pub(crate) fn run(self) -> Result<ScriptedRun, TuiError> {
        let shared = Rc::new(RefCell::new(SharedScriptState::new(self.steps)));
        let mut runtime = ScriptedRuntime::new(self.width, self.height, Rc::clone(&shared));
        let terminal = runtime.init_terminal()?;
        let mut session = TerminalSession::new(&mut runtime, terminal);
        let mut observer = ScriptObserver::new(Rc::clone(&shared));
        let outcome = match event_loop_with_observer(
            &self.command,
            &self.config,
            &mut session,
            &mut observer,
        ) {
            Ok(argv) => ScriptedOutcome::Run(
                argv.into_iter()
                    .map(|token| token.to_string_lossy().to_string())
                    .collect(),
            ),
            Err(TuiError::Cancelled) => ScriptedOutcome::Cancelled,
            Err(err) => return Err(err),
        };
        let frames = shared.borrow().frames.clone();

        Ok(ScriptedRun { outcome, frames })
    }
}

#[derive(Debug)]
struct SharedScriptState {
    steps: VecDeque<ScriptedStep>,
    queued_events: VecDeque<AppEvent>,
    frames: Vec<ObservedFrame>,
}

impl SharedScriptState {
    fn new(steps: Vec<ScriptedStep>) -> Self {
        Self {
            steps: steps.into(),
            queued_events: VecDeque::new(),
            frames: Vec::new(),
        }
    }

    fn next_step_description(&self) -> String {
        self.steps
            .front()
            .map_or_else(|| "application exit".to_string(), ScriptedStep::description)
    }

    fn record_frame(&mut self, backend: &TestBackend, snapshot: &FrameSnapshot) {
        self.frames.push(ObservedFrame {
            text: buffer_text(backend),
            snapshot: snapshot.clone(),
        });
    }

    fn resolve_steps(&mut self, backend: &TestBackend) {
        let Some(frame) = self.frames.last() else {
            return;
        };

        while let Some(step) = self.steps.front() {
            match resolve_step(step, backend, frame) {
                StepResolution::Satisfied => {
                    self.steps.pop_front();
                }
                StepResolution::Queue(events) => {
                    self.queued_events.extend(events);
                    self.steps.pop_front();
                    break;
                }
                StepResolution::Pending => break,
            }
        }
    }
}

#[derive(Debug)]
struct ScriptedRuntime {
    width: u16,
    height: u16,
    shared: Rc<RefCell<SharedScriptState>>,
}

impl ScriptedRuntime {
    fn new(width: u16, height: u16, shared: Rc<RefCell<SharedScriptState>>) -> Self {
        Self {
            width,
            height,
            shared,
        }
    }
}

impl Runtime for ScriptedRuntime {
    type Backend = TestBackend;

    fn init_terminal(&mut self) -> Result<Terminal<Self::Backend>, TuiError> {
        let Ok(terminal) = Terminal::new(TestBackend::new(self.width, self.height));
        Ok(terminal)
    }

    fn restore_terminal(&mut self, _terminal: &mut Terminal<Self::Backend>) {}

    fn poll_event(&mut self, _timeout: Duration) -> Result<bool, TuiError> {
        let shared = self.shared.borrow();
        if !shared.queued_events.is_empty() {
            return Ok(true);
        }
        Err(TuiError::from(io::Error::other(format!(
            "script stalled waiting for {}",
            shared.next_step_description()
        ))))
    }

    fn read_event(&mut self) -> Result<AppEvent, TuiError> {
        self.shared
            .borrow_mut()
            .queued_events
            .pop_front()
            .ok_or_else(|| {
                TuiError::from(io::Error::other(
                    "script runtime tried to read an event from an empty queue",
                ))
            })
    }

    fn copy_to_clipboard(&mut self, _text: &str) -> Result<(), String> {
        Ok(())
    }
}

struct ScriptObserver {
    shared: Rc<RefCell<SharedScriptState>>,
}

impl ScriptObserver {
    fn new(shared: Rc<RefCell<SharedScriptState>>) -> Self {
        Self { shared }
    }
}

impl DrawObserver<TestBackend> for ScriptObserver {
    fn observe(
        &mut self,
        backend: &TestBackend,
        frame_snapshot: &FrameSnapshot,
    ) -> Result<(), TuiError> {
        let mut shared = self.shared.borrow_mut();
        shared.record_frame(backend, frame_snapshot);
        shared.resolve_steps(backend);
        Ok(())
    }
}

enum StepResolution {
    Satisfied,
    Queue(Vec<AppEvent>),
    Pending,
}

fn resolve_step(
    step: &ScriptedStep,
    backend: &TestBackend,
    frame: &ObservedFrame,
) -> StepResolution {
    match step {
        ScriptedStep::ExpectText(text) => {
            if frame.text.contains(text) {
                StepResolution::Satisfied
            } else {
                StepResolution::Pending
            }
        }
        ScriptedStep::Event(event) => StepResolution::Queue(vec![event.clone()]),
        ScriptedStep::TypeText(text) => StepResolution::Queue(
            text.chars()
                .map(|ch| key_event(AppKeyCode::Char(ch)))
                .collect(),
        ),
        ScriptedStep::Paste(text) => StepResolution::Queue(vec![AppEvent::Paste(text.clone())]),
        ScriptedStep::ClickFooter(target) => frame
            .snapshot
            .layout
            .footer_buttons
            .iter()
            .find(|button| button.target == *target)
            .map_or(StepResolution::Pending, |button| {
                StepResolution::Queue(vec![mouse_down(center_of(button.rect))])
            }),
        ScriptedStep::ClickInput(arg_id) => frame
            .snapshot
            .form_input_rect(arg_id)
            .map_or(StepResolution::Pending, |rect| {
                StepResolution::Queue(vec![mouse_down(center_of(rect))])
            }),
        ScriptedStep::SelectDropdownOption(label) => {
            dropdown_row_for_label(backend, frame.snapshot.layout.dropdown, label)
                .map_or(StepResolution::Pending, |row| {
                    StepResolution::Queue(vec![mouse_down((row.0, row.1))])
                })
        }
        ScriptedStep::Resize { width, height } => StepResolution::Queue(vec![AppEvent::Resize {
            width: *width,
            height: *height,
        }]),
    }
}

fn key_event(code: AppKeyCode) -> AppEvent {
    AppEvent::Key(AppKeyEvent::new(code, AppKeyModifiers::default()))
}

fn ctrl_event(code: AppKeyCode) -> AppEvent {
    AppEvent::Key(AppKeyEvent::new(
        code,
        AppKeyModifiers {
            control: true,
            ..AppKeyModifiers::default()
        },
    ))
}

fn mouse_down((column, row): (u16, u16)) -> AppEvent {
    AppEvent::Mouse(AppMouseEvent {
        kind: AppMouseEventKind::Down(AppMouseButton::Left),
        column,
        row,
        modifiers: AppKeyModifiers::default(),
    })
}

fn center_of(rect: Rect) -> (u16, u16) {
    let x = rect.x.saturating_add(rect.width.saturating_sub(1) / 2);
    let y = rect.y.saturating_add(rect.height.saturating_sub(1) / 2);
    (x, y)
}

fn dropdown_row_for_label(
    backend: &TestBackend,
    area: Option<Rect>,
    label: &str,
) -> Option<(u16, u16)> {
    let area = area?;
    for row in area.y.saturating_add(1)..area.y.saturating_add(area.height.saturating_sub(1)) {
        let line = buffer_row_text(backend, area, row);
        if line.contains(label) {
            return Some((area.x.saturating_add(1), row));
        }
    }
    None
}

fn buffer_row_text(backend: &TestBackend, area: Rect, row: u16) -> String {
    let mut line = String::new();
    for column in area.x..area.x.saturating_add(area.width) {
        line.push_str(backend.buffer()[(column, row)].symbol());
    }
    line
}

fn buffer_text(backend: &TestBackend) -> String {
    backend
        .buffer()
        .content
        .iter()
        .map(ratatui::buffer::Cell::symbol)
        .collect::<String>()
}
