use crate::models::radarr_models::{
  AddMovieBody, AddMovieSearchResult, Credit, CreditType, DeleteMovieParams, DownloadRecord,
  EditMovieParams, Movie, MovieCommandBody, MovieHistoryItem, RadarrRelease,
  RadarrReleaseDownloadBody,
};
use crate::models::servarr_data::Notification;
use crate::models::servarr_data::radarr::modals::MovieDetailsModal;
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
use crate::models::stateful_table::StatefulTable;
use crate::models::{Route, ScrollableText};
use crate::network::radarr_network::RadarrEvent;
use crate::network::{Network, RequestMethod};
use crate::utils::{convert_runtime, convert_to_gb};
use anyhow::Result;
use indoc::formatdoc;
use log::{debug, info, warn};
use serde_json::{Value, json};
use urlencoding::encode;

#[cfg(test)]
#[path = "radarr_library_network_tests.rs"]
mod radarr_library_network_tests;

impl Network<'_, '_> {
  pub(in crate::network::radarr_network) async fn add_movie(
    &mut self,
    mut add_movie_body: AddMovieBody,
  ) -> Result<Value> {
    info!("Adding new movie to Radarr");
    let event = RadarrEvent::AddMovie(AddMovieBody::default());
    if let Some(tag_input_str) = add_movie_body.tag_input_string.as_ref() {
      let tag_ids_vec = self.extract_and_add_radarr_tag_ids_vec(tag_input_str).await;
      add_movie_body.tags = tag_ids_vec;
    }

    debug!("Add movie body: {add_movie_body:?}");

    let request_props = self
      .request_props_from(event, RequestMethod::Post, Some(add_movie_body), None, None)
      .await;

    self
      .handle_request::<AddMovieBody, Value>(request_props, |_, _| ())
      .await
  }

  pub(in crate::network::radarr_network) async fn delete_movie(
    &mut self,
    delete_movie_params: DeleteMovieParams,
  ) -> Result<()> {
    let event = RadarrEvent::DeleteMovie(DeleteMovieParams::default());
    let DeleteMovieParams {
      id,
      delete_movie_files,
      add_list_exclusion,
    } = delete_movie_params;
    info!(
      "Deleting Radarr movie with ID: {id} with deleteFiles={delete_movie_files} and addImportExclusion={add_list_exclusion}"
    );

    let request_props = self
      .request_props_from(
        event,
        RequestMethod::Delete,
        None::<()>,
        Some(format!("/{id}")),
        Some(format!(
          "deleteFiles={delete_movie_files}&addImportExclusion={add_list_exclusion}"
        )),
      )
      .await;

    self
      .handle_request::<(), ()>(request_props, |_, _| ())
      .await
  }

  pub(in crate::network::radarr_network) async fn download_radarr_release(
    &mut self,
    params: RadarrReleaseDownloadBody,
  ) -> Result<Value> {
    let event = RadarrEvent::DownloadRelease(RadarrReleaseDownloadBody::default());
    info!("Downloading Radarr release with params: {params:?}");

    let request_props = self
      .request_props_from(event, RequestMethod::Post, Some(params), None, None)
      .await;

    let result = self
      .handle_request::<RadarrReleaseDownloadBody, Value>(request_props, |_, mut app| {
        app.notification = Some(Notification::new(
          "Download Result".to_owned(),
          "Download request sent successfully".to_owned(),
          true,
        ));
      })
      .await;

    if result.is_err() {
      let mut app = self.app.lock().await;
      std::mem::take(&mut app.error.text);
      app.notification = Some(Notification::new(
        "Download Failed".to_owned(),
        "Download request failed. Check the logs for more details.".to_owned(),
        false,
      ));
    }

    result
  }

  pub(in crate::network::radarr_network) async fn edit_movie(
    &mut self,
    mut edit_movie_params: EditMovieParams,
  ) -> Result<()> {
    info!("Editing Radarr movie");
    let movie_id = edit_movie_params.movie_id;
    let detail_event = RadarrEvent::GetMovieDetails(movie_id);
    let event = RadarrEvent::EditMovie(EditMovieParams::default());
    if let Some(tag_input_str) = edit_movie_params.tag_input_string.as_ref() {
      let tag_ids_vec = self.extract_and_add_radarr_tag_ids_vec(tag_input_str).await;
      edit_movie_params.tags = Some(tag_ids_vec);
    }

    info!("Fetching movie details for movie with ID: {movie_id}");

    let request_props = self
      .request_props_from(
        detail_event,
        RequestMethod::Get,
        None::<()>,
        Some(format!("/{movie_id}")),
        None,
      )
      .await;

    let mut response = String::new();

    self
      .handle_request::<(), Value>(request_props, |detailed_movie_body, _| {
        response = detailed_movie_body.to_string()
      })
      .await?;

    info!("Constructing edit movie body");

    let mut detailed_movie_body: Value = serde_json::from_str(&response)?;
    let (monitored, minimum_availability, quality_profile_id, root_folder_path, tags) = {
      let monitored = edit_movie_params.monitored.unwrap_or(
        detailed_movie_body["monitored"]
          .as_bool()
          .expect("Unable to deserialize 'monitored'"),
      );
      let minimum_availability = edit_movie_params
        .minimum_availability
        .unwrap_or_else(|| {
          serde_json::from_value(detailed_movie_body["minimumAvailability"].clone())
            .expect("Unable to deserialize 'minimumAvailability'")
        })
        .to_string();
      let quality_profile_id = edit_movie_params.quality_profile_id.unwrap_or_else(|| {
        detailed_movie_body["qualityProfileId"]
          .as_i64()
          .expect("Unable to deserialize 'qualityProfileId'")
      });
      let root_folder_path = edit_movie_params.root_folder_path.unwrap_or_else(|| {
        detailed_movie_body["path"]
          .as_str()
          .expect("Unable to deserialize 'path'")
          .to_owned()
      });
      let tags = if edit_movie_params.clear_tags {
        vec![]
      } else {
        edit_movie_params.tags.unwrap_or(
          detailed_movie_body["tags"]
            .as_array()
            .expect("Unable to deserialize 'tags'")
            .iter()
            .map(|item| item.as_i64().expect("Unable to deserialize tag ID"))
            .collect(),
        )
      };

      (
        monitored,
        minimum_availability,
        quality_profile_id,
        root_folder_path,
        tags,
      )
    };

    *detailed_movie_body.get_mut("monitored").unwrap() = json!(monitored);
    *detailed_movie_body.get_mut("minimumAvailability").unwrap() = json!(minimum_availability);
    *detailed_movie_body.get_mut("qualityProfileId").unwrap() = json!(quality_profile_id);
    *detailed_movie_body.get_mut("path").unwrap() = json!(root_folder_path);
    *detailed_movie_body.get_mut("tags").unwrap() = json!(tags);

    debug!("Edit movie body: {detailed_movie_body:?}");

    let request_props = self
      .request_props_from(
        event,
        RequestMethod::Put,
        Some(detailed_movie_body),
        Some(format!("/{movie_id}")),
        None,
      )
      .await;

    self
      .handle_request::<Value, ()>(request_props, |_, _| ())
      .await
  }

  pub(in crate::network::radarr_network) async fn get_credits(
    &mut self,
    movie_id: i64,
  ) -> Result<Vec<Credit>> {
    info!("Fetching Radarr movie credits");
    let event = RadarrEvent::GetMovieCredits(movie_id);

    let request_props = self
      .request_props_from(
        event,
        RequestMethod::Get,
        None::<()>,
        None,
        Some(format!("movieId={movie_id}")),
      )
      .await;

    self
      .handle_request::<(), Vec<Credit>>(request_props, |credit_vec, mut app| {
        let cast_vec: Vec<Credit> = credit_vec
          .iter()
          .filter(|&credit| credit.credit_type == CreditType::Cast)
          .cloned()
          .collect();
        let crew_vec: Vec<Credit> = credit_vec
          .iter()
          .filter(|&credit| credit.credit_type == CreditType::Crew)
          .cloned()
          .collect();

        let modal = app
          .data
          .radarr_data
          .movie_details_modal
          .get_or_insert_default();

        modal.movie_cast.set_items(cast_vec);
        modal.movie_crew.set_items(crew_vec);
      })
      .await
  }

  pub(in crate::network::radarr_network) async fn get_movies(&mut self) -> Result<Vec<Movie>> {
    info!("Fetching Radarr library");
    let event = RadarrEvent::GetMovies;

    let request_props = self
      .request_props_from(event, RequestMethod::Get, None::<()>, None, None)
      .await;

    self
      .handle_request::<(), Vec<Movie>>(request_props, |mut movie_vec, mut app| {
        if !matches!(
          app.get_current_route(),
          Route::Radarr(ActiveRadarrBlock::MoviesSortPrompt, _)
        ) {
          movie_vec.sort_by_key(|a| a.id);
          app.data.radarr_data.movies.set_items(movie_vec);
          app.data.radarr_data.movies.apply_sorting_toggle(false);
        }
      })
      .await
  }

  pub(in crate::network::radarr_network) async fn get_movie_details(
    &mut self,
    movie_id: i64,
  ) -> Result<Movie> {
    info!("Fetching Radarr movie details");
    let event = RadarrEvent::GetMovieDetails(movie_id);

    info!("Fetching movie details for movie with ID: {movie_id}");

    let request_props = self
      .request_props_from(
        event,
        RequestMethod::Get,
        None::<()>,
        Some(format!("/{movie_id}")),
        None,
      )
      .await;

    self
      .handle_request::<(), Movie>(request_props, |movie_response, mut app| {
        let Movie {
          id,
          title,
          year,
          overview,
          path,
          studio,
          has_file,
          quality_profile_id,
          size_on_disk,
          genres,
          runtime,
          certification,
          ratings,
          movie_file,
          collection,
          ..
        } = movie_response;
        let (hours, minutes) = convert_runtime(runtime);
        let size = convert_to_gb(size_on_disk);
        let studio = studio.clone().unwrap_or_default();
        let quality_profile = app
          .data
          .radarr_data
          .quality_profile_map
          .get_by_left(&quality_profile_id)
          .unwrap_or(&"".to_owned())
          .to_owned();
        let imdb_rating = if let Some(rating) = ratings.imdb {
          if let Some(value) = rating.value.as_f64() {
            format!("{value:.1}")
          } else {
            String::new()
          }
        } else {
          String::new()
        };

        let tmdb_rating = if let Some(rating) = ratings.tmdb {
          if let Some(value) = rating.value.as_f64() {
            format!("{}%", (value * 10f64).ceil())
          } else {
            String::new()
          }
        } else {
          String::new()
        };

        let rotten_tomatoes_rating = if let Some(rating) = ratings.rotten_tomatoes {
          if let Some(value) = rating.value.as_u64() {
            format!("{value}%")
          } else {
            String::new()
          }
        } else {
          String::new()
        };

        let status = get_movie_status(has_file, &app.data.radarr_data.downloads.items, id);
        let collection = collection.unwrap_or_default();

        let mut movie_details_modal = MovieDetailsModal {
          movie_details: ScrollableText::with_string(formatdoc!(
            "Title: {title}
            Year: {year}
            Runtime: {hours}h {minutes}m
            Rating: {}
            Collection: {}
            Status: {status}
            Description: {overview}
            TMDB: {tmdb_rating}
            IMDB: {imdb_rating}
            Rotten Tomatoes: {rotten_tomatoes_rating}
            Quality Profile: {quality_profile}
            Size: {size:.2} GB
            Path: {path}
            Studio: {studio}
            Genres: {}",
            certification.unwrap_or_default(),
            collection
              .title
              .as_ref()
              .unwrap_or(&String::new())
              .to_owned(),
            genres.join(", ")
          )),
          ..MovieDetailsModal::default()
        };

        if let Some(file) = movie_file {
          movie_details_modal.file_details = formatdoc!(
            "Relative Path: {}
              Absolute Path: {}
              Size: {size:.2} GB
              Date Added: {}",
            file.relative_path,
            file.path,
            file.date_added
          );

          if let Some(media_info) = file.media_info {
            movie_details_modal.audio_details = formatdoc!(
              "Bitrate: {}
              Channels: {:.1}
              Codec: {}
              Languages: {}
              Stream Count: {}",
              media_info.audio_bitrate,
              media_info.audio_channels.as_f64().unwrap(),
              media_info.audio_codec.unwrap_or_default(),
              media_info.audio_languages.unwrap_or_default(),
              media_info.audio_stream_count
            );

            movie_details_modal.video_details = formatdoc!(
              "Bit Depth: {}
              Bitrate: {}
              Codec: {}
              FPS: {}
              Resolution: {}
              Scan Type: {}
              Runtime: {}",
              media_info.video_bit_depth,
              media_info.video_bitrate,
              media_info.video_codec.unwrap_or_default(),
              media_info.video_fps.as_f64().unwrap(),
              media_info.resolution,
              media_info.scan_type,
              media_info.run_time
            );
          }
        }

        app.data.radarr_data.movie_details_modal = Some(movie_details_modal);
      })
      .await
  }

  pub(in crate::network::radarr_network) async fn get_movie_history(
    &mut self,
    movie_id: i64,
  ) -> Result<Vec<MovieHistoryItem>> {
    info!("Fetching Radarr movie history");
    let event = RadarrEvent::GetMovieHistory(movie_id);

    let request_props = self
      .request_props_from(
        event,
        RequestMethod::Get,
        None::<()>,
        None,
        Some(format!("movieId={movie_id}")),
      )
      .await;

    self
      .handle_request::<(), Vec<MovieHistoryItem>>(request_props, |movie_history_vec, mut app| {
        let mut reversed_movie_history_vec = movie_history_vec.to_vec();
        reversed_movie_history_vec.reverse();

        app
          .data
          .radarr_data
          .movie_details_modal
          .get_or_insert_default()
          .movie_history
          .set_items(reversed_movie_history_vec)
      })
      .await
  }

  pub(in crate::network::radarr_network) async fn get_movie_releases(
    &mut self,
    movie_id: i64,
  ) -> Result<Vec<RadarrRelease>> {
    info!("Fetching releases for movie with ID: {movie_id}");
    let event = RadarrEvent::GetReleases(movie_id);

    let request_props = self
      .request_props_from(
        event,
        RequestMethod::Get,
        None::<()>,
        None,
        Some(format!("movieId={movie_id}")),
      )
      .await;

    self
      .handle_request::<(), Vec<RadarrRelease>>(request_props, |release_vec, mut app| {
        app
          .data
          .radarr_data
          .movie_details_modal
          .get_or_insert_default()
          .movie_releases
          .set_items(release_vec);
      })
      .await
  }

  pub(in crate::network::radarr_network) async fn search_movie(
    &mut self,
    query: String,
  ) -> Result<Vec<AddMovieSearchResult>> {
    info!("Searching for specific Radarr movie");
    let event = RadarrEvent::SearchNewMovie(String::new());

    let request_props = self
      .request_props_from(
        event,
        RequestMethod::Get,
        None::<()>,
        None,
        Some(format!("term={}", encode(&query))),
      )
      .await;

    let result = self
      .handle_request::<(), Vec<AddMovieSearchResult>>(request_props, |movie_vec, mut app| {
        if movie_vec.is_empty() {
          app.pop_and_push_navigation_stack(ActiveRadarrBlock::AddMovieEmptySearchResults.into());
        } else if let Some(add_searched_movies) = app.data.radarr_data.add_searched_movies.as_mut()
        {
          add_searched_movies.set_items(movie_vec);
        } else {
          let mut add_searched_movies = StatefulTable::default();
          add_searched_movies.set_items(movie_vec);
          app.data.radarr_data.add_searched_movies = Some(add_searched_movies);
        }
      })
      .await;

    if result.is_err() {
      self.app.lock().await.data.radarr_data.add_searched_movies = Some(StatefulTable::default());
    }

    result
  }

  pub(in crate::network) async fn toggle_movie_monitoring(&mut self, movie_id: i64) -> Result<()> {
    let event = RadarrEvent::ToggleMovieMonitoring(movie_id);

    let detail_event = RadarrEvent::GetMovieDetails(movie_id);
    info!("Toggling movie monitoring for movie with ID: {movie_id}");
    info!("Fetching movie details for movie with ID: {movie_id}");

    let request_props = self
      .request_props_from(
        detail_event,
        RequestMethod::Get,
        None::<()>,
        Some(format!("/{movie_id}")),
        None,
      )
      .await;

    let mut response = String::new();

    self
      .handle_request::<(), Value>(request_props, |detailed_movie_body, _| {
        response = detailed_movie_body.to_string()
      })
      .await?;

    info!("Constructing toggle movie monitoring body");

    match serde_json::from_str::<Value>(&response) {
      Ok(mut detailed_movie_body) => {
        let monitored = detailed_movie_body
          .get("monitored")
          .unwrap()
          .as_bool()
          .unwrap();

        *detailed_movie_body.get_mut("monitored").unwrap() = json!(!monitored);

        debug!("Toggle movie monitoring body: {detailed_movie_body:?}");

        let request_props = self
          .request_props_from(
            event,
            RequestMethod::Put,
            Some(detailed_movie_body),
            Some(format!("/{movie_id}")),
            None,
          )
          .await;

        self
          .handle_request::<Value, ()>(request_props, |_, _| ())
          .await
      }
      Err(_) => {
        warn!("Request for detailed movie body was interrupted");
        Ok(())
      }
    }
  }

  pub(in crate::network) async fn trigger_automatic_movie_search(
    &mut self,
    movie_id: i64,
  ) -> Result<Value> {
    let event = RadarrEvent::TriggerAutomaticSearch(movie_id);
    info!("Searching indexers for movie with ID: {movie_id}");
    let body = MovieCommandBody {
      name: "MoviesSearch".to_owned(),
      movie_ids: vec![movie_id],
    };

    let request_props = self
      .request_props_from(event, RequestMethod::Post, Some(body), None, None)
      .await;

    self
      .handle_request::<MovieCommandBody, Value>(request_props, |_, _| ())
      .await
  }

  pub(in crate::network) async fn update_all_movies(&mut self) -> Result<Value> {
    info!("Updating all movies");
    let event = RadarrEvent::UpdateAllMovies;
    let body = MovieCommandBody {
      name: "RefreshMovie".to_owned(),
      movie_ids: Vec::new(),
    };

    let request_props = self
      .request_props_from(event, RequestMethod::Post, Some(body), None, None)
      .await;

    self
      .handle_request::<MovieCommandBody, Value>(request_props, |_, _| ())
      .await
  }

  pub(in crate::network) async fn update_and_scan_movie(&mut self, movie_id: i64) -> Result<Value> {
    let event = RadarrEvent::UpdateAndScan(movie_id);
    info!("Updating and scanning movie with ID: {movie_id}");
    let body = MovieCommandBody {
      name: "RefreshMovie".to_owned(),
      movie_ids: vec![movie_id],
    };

    let request_props = self
      .request_props_from(event, RequestMethod::Post, Some(body), None, None)
      .await;

    self
      .handle_request::<MovieCommandBody, Value>(request_props, |_, _| ())
      .await
  }
}

pub(in crate::network::radarr_network::library) fn get_movie_status(
  has_file: bool,
  downloads_vec: &[DownloadRecord],
  movie_id: i64,
) -> String {
  if !has_file {
    if let Some(download) = downloads_vec
      .iter()
      .find(|&download| download.movie_id == movie_id)
    {
      if download.status == "downloading" {
        return "Downloading".to_owned();
      }

      if download.status == "completed" {
        return "Awaiting Import".to_owned();
      }
    }

    return "Missing".to_owned();
  }

  "Downloaded".to_owned()
}
