#![warn(clippy::all)]

use crate::search::SearchRes;

use tvrank::imdb::{ImdbQuery, ImdbTitle};

use humantime::format_duration;
use prettytable::{color, format, Attr, Cell, Row, Table};
use reqwest::Url;
use serde::Serialize;
use truncatable::Truncatable;

#[derive(Debug, thiserror::Error)]
#[error("Output printing error")]
pub enum Error {
  #[error("JSON output error: {0}")]
  Json(#[from] serde_json::Error),
  #[error("YAML output error: {0}")]
  Yaml(#[from] serde_yaml::Error),
  #[error("Table output error: {0}")]
  Table(#[from] url::ParseError),
}

#[derive(Debug, Clone, clap::ValueEnum)]
pub enum OutputFormat {
  Json,
  Table,
  Yaml,
}

#[derive(Serialize)]
struct OutputWrapper<'search_res, 'a, 'storage> {
  movies: Option<&'search_res [&'a ImdbTitle<'storage>]>,
  series: Option<&'search_res [&'a ImdbTitle<'storage>]>,
}

impl<'search_res, 'a, 'storage> OutputWrapper<'search_res, 'a, 'storage> {
  fn new(
    movies: Option<&'search_res [&'a ImdbTitle<'storage>]>,
    series: Option<&'search_res [&'a ImdbTitle<'storage>]>,
  ) -> Self {
    Self { movies, series }
  }
}

pub trait Printer {
  type Error;

  fn get_format(&self) -> OutputFormat;

  fn print(
    &self,
    movies: Option<SearchRes>,
    series: Option<SearchRes>,
    imdb_url: &Url,
    search_terms: Option<&str>,
  ) -> Result<(), Self::Error>;
}

pub struct JsonPrinter;

impl JsonPrinter {
  #[must_use]
  pub fn new() -> Self {
    Self
  }
}

impl Printer for JsonPrinter {
  type Error = Error;

  fn get_format(&self) -> OutputFormat {
    OutputFormat::Json
  }

  fn print(
    &self,
    mut movies: Option<SearchRes>,
    mut series: Option<SearchRes>,
    _imdb_url: &Url,
    _search_terms: Option<&str>,
  ) -> Result<(), Self::Error> {
    let movie_results = movies.as_mut().map(|movies| movies.top_sorted_results());
    let series_results = series.as_mut().map(|series| series.top_sorted_results());
    println!("{}", serde_json::to_string_pretty(&OutputWrapper::new(movie_results, series_results))?);
    Ok(())
  }
}

pub struct YamlPrinter;

impl YamlPrinter {
  #[must_use]
  pub fn new() -> Self {
    Self
  }
}

impl Printer for YamlPrinter {
  type Error = Error;

  fn get_format(&self) -> OutputFormat {
    OutputFormat::Yaml
  }

  fn print(
    &self,
    mut movies: Option<SearchRes>,
    mut series: Option<SearchRes>,
    _imdb_url: &Url,
    _search_terms: Option<&str>,
  ) -> Result<(), Self::Error> {
    let movie_results = movies.as_mut().map(|movies| movies.top_sorted_results());
    let series_results = series.as_mut().map(|series| series.top_sorted_results());
    println!("{}", serde_yaml::to_string(&OutputWrapper::new(movie_results, series_results,))?);
    Ok(())
  }
}

#[derive(Clone)]
pub struct TablePrinter {
  color: bool,
}

impl Printer for TablePrinter {
  type Error = Error;

  fn get_format(&self) -> OutputFormat {
    OutputFormat::Table
  }

  fn print(
    &self,
    movies: Option<SearchRes>,
    series: Option<SearchRes>,
    imdb_url: &Url,
    search_terms: Option<&str>,
  ) -> Result<(), Self::Error> {
    if let Some(movies) = movies {
      self.print_results(movies, imdb_url, ImdbQuery::Movies, search_terms)?;
    }
    if let Some(series) = series {
      self.print_results(series, imdb_url, ImdbQuery::Series, search_terms)?;
    }

    Ok(())
  }
}

impl TablePrinter {
  #[must_use]
  pub fn new(color: bool) -> Self {
    Self { color }
  }

  fn print_results(
    &self,
    mut results: SearchRes,
    imdb_url: &Url,
    query: ImdbQuery,
    search_terms: Option<&str>,
  ) -> Result<(), Error> {
    if results.is_empty() {
      if let Some(search_terms) = search_terms {
        eprintln!("No {query} matches found for `{search_terms}`");
      } else {
        eprintln!("No {query} matches found");
      }
    } else {
      let num = results.total_len();
      let matches = if num == 1 {
        "match"
      } else {
        "matches"
      };

      if let Some(search_terms) = search_terms {
        if results.is_truncated() {
          println!("Found {num} {query} {matches} for `{search_terms}`, {} will be displayed:", results.len())
        } else {
          println!("Found {num} {query} {matches} for `{search_terms}`:")
        }
      } else if results.is_truncated() {
        println!("Found {num} {query} {matches}, {} will be displayed:", results.len());
      } else {
        println!("Found {num} {query} {matches}:");
      }

      let mut table = create_table(self.color);

      for res in results.top_sorted_results() {
        let row = self.create_table_row(res, imdb_url)?;
        table.add_row(row);
      }
      table.printstd();
      println!();
    }

    Ok(())
  }

  fn create_table_row(&self, title: &ImdbTitle, imdb_url: &Url) -> Result<Row, Error> {
    static GREEN: Attr = Attr::ForegroundColor(color::GREEN);
    static YELLOW: Attr = Attr::ForegroundColor(color::YELLOW);
    static RED: Attr = Attr::ForegroundColor(color::RED);

    let mut row = Row::new(vec![]);

    row.add_cell(Cell::new(&Truncatable::from(title.primary_title()).truncate(50)));

    if let Some(original_title) = title.original_title() {
      row.add_cell(Cell::new(&Truncatable::from(original_title).truncate(30)));
    } else {
      row.add_cell(Cell::new(""));
    }

    if let Some(year) = title.start_year() {
      row.add_cell(Cell::new(&format!("{year}")));
    } else {
      row.add_cell(Cell::new(""));
    }

    if let Some(rating) = title.rating() {
      let rating_text = &format!("{}/100", rating.rating());

      let mut rating_cell = Cell::new(rating_text);
      if self.color {
        rating_cell = rating_cell.with_style(match rating {
          rating if rating.rating() >= 70 => GREEN,
          rating if (60..70).contains(&rating.rating()) => YELLOW,
          _ => RED,
        });
      }

      row.add_cell(rating_cell);
      row.add_cell(Cell::new(&format!("{}", rating.votes())));
    } else {
      row.add_cell(Cell::new(""));
      row.add_cell(Cell::new(""));
    }

    if let Some(runtime) = title.runtime() {
      row.add_cell(Cell::new(&format_duration(runtime).to_string()));
    } else {
      row.add_cell(Cell::new(""));
    }

    row.add_cell(Cell::new(&format!("{}", title.genres())));
    row.add_cell(Cell::new(&format!("{}", title.title_type())));

    let title_id = title.title_id();
    row.add_cell(Cell::new(&format!("{title_id}")));

    let url = imdb_url.join(&format!("{title_id}"))?;
    row.add_cell(Cell::new(url.as_str()));

    Ok(row)
  }
}

#[macro_export]
macro_rules! make_bold {
  ($title: expr, $color: expr) => {
    match $color {
      true => Cell::new($title).with_style(Attr::Bold),
      false => Cell::new($title),
    }
  };
}

fn create_table(color: bool) -> Table {
  let mut table = Table::new();

  let table_format = format::FormatBuilder::new()
    .column_separator('│')
    .borders('│')
    .padding(1, 1)
    .build();

  table.set_format(table_format);

  table.add_row(Row::new(vec![
    make_bold!("Primary Title", color),
    make_bold!("Original Title", color),
    make_bold!("Year", color),
    make_bold!("Rating", color),
    make_bold!("Votes", color),
    make_bold!("Runtime", color),
    make_bold!("Genres", color),
    make_bold!("Type", color),
    make_bold!("IMDB ID", color),
    make_bold!("IMDB Link", color),
  ]));

  table
}
