use std::{cmp::Ordering, collections::HashMap};

use comfy_table::{Attribute, Cell, Color};
use futures_util::{FutureExt, try_join};
use komodo_client::{
  KomodoClient,
  api::read::{
    ListActions, ListAlerters, ListBuilders, ListBuilds,
    ListDeployments, ListProcedures, ListRepos, ListResourceSyncs,
    ListSchedules, ListServers, ListStacks, ListTags,
  },
  entities::{
    ResourceTargetVariant,
    action::{ActionListItem, ActionListItemInfo, ActionState},
    alerter::{AlerterListItem, AlerterListItemInfo},
    build::{BuildListItem, BuildListItemInfo, BuildState},
    builder::{BuilderListItem, BuilderListItemInfo},
    config::cli::args::{
      self,
      list::{ListCommand, ResourceFilters},
    },
    deployment::{
      DeploymentListItem, DeploymentListItemInfo, DeploymentState,
    },
    procedure::{
      ProcedureListItem, ProcedureListItemInfo, ProcedureState,
    },
    repo::{RepoListItem, RepoListItemInfo, RepoState},
    resource::{ResourceListItem, ResourceQuery},
    resource_link,
    schedule::Schedule,
    server::{ServerListItem, ServerListItemInfo, ServerState},
    stack::{StackListItem, StackListItemInfo, StackState},
    sync::{
      ResourceSyncListItem, ResourceSyncListItemInfo,
      ResourceSyncState,
    },
  },
};
use serde::Serialize;

use crate::{
  command::{
    PrintTable, format_timetamp, matches_wildcards, parse_wildcards,
    print_items,
  },
  config::cli_config,
};

pub async fn handle(list: &args::list::List) -> anyhow::Result<()> {
  match &list.command {
    None => list_all(list).await,
    Some(ListCommand::Servers(filters)) => {
      list_resources::<ServerListItem>(filters, false).await
    }
    Some(ListCommand::Stacks(filters)) => {
      list_resources::<StackListItem>(filters, false).await
    }
    Some(ListCommand::Deployments(filters)) => {
      list_resources::<DeploymentListItem>(filters, false).await
    }
    Some(ListCommand::Builds(filters)) => {
      list_resources::<BuildListItem>(filters, false).await
    }
    Some(ListCommand::Repos(filters)) => {
      list_resources::<RepoListItem>(filters, false).await
    }
    Some(ListCommand::Procedures(filters)) => {
      list_resources::<ProcedureListItem>(filters, false).await
    }
    Some(ListCommand::Actions(filters)) => {
      list_resources::<ActionListItem>(filters, false).await
    }
    Some(ListCommand::Syncs(filters)) => {
      list_resources::<ResourceSyncListItem>(filters, false).await
    }
    Some(ListCommand::Builders(filters)) => {
      list_resources::<BuilderListItem>(filters, false).await
    }
    Some(ListCommand::Alerters(filters)) => {
      list_resources::<AlerterListItem>(filters, false).await
    }
    Some(ListCommand::Schedules(filters)) => {
      list_schedules(filters).await
    }
  }
}

/// Includes all resources besides builds and alerters.
async fn list_all(list: &args::list::List) -> anyhow::Result<()> {
  let filters: ResourceFilters = list.clone().into();
  let client = super::komodo_client().await?;
  let (
    tags,
    mut servers,
    mut stacks,
    mut deployments,
    mut builds,
    mut repos,
    mut procedures,
    mut actions,
    mut syncs,
  ) = try_join!(
    client.read(ListTags::default()).map(|res| res.map(|res| res
      .into_iter()
      .map(|t| (t.id, t.name))
      .collect::<HashMap<_, _>>())),
    ServerListItem::list(client, &filters, true),
    StackListItem::list(client, &filters, true),
    DeploymentListItem::list(client, &filters, true),
    BuildListItem::list(client, &filters, true),
    RepoListItem::list(client, &filters, true),
    ProcedureListItem::list(client, &filters, true),
    ActionListItem::list(client, &filters, true),
    ResourceSyncListItem::list(client, &filters, true),
  )?;

  if !servers.is_empty() {
    fix_tags(&mut servers, &tags);
    print_items(servers, filters.format, list.links)?;
    println!();
  }

  if !stacks.is_empty() {
    fix_tags(&mut stacks, &tags);
    print_items(stacks, filters.format, list.links)?;
    println!();
  }

  if !deployments.is_empty() {
    fix_tags(&mut deployments, &tags);
    print_items(deployments, filters.format, list.links)?;
    println!();
  }

  if !builds.is_empty() {
    fix_tags(&mut builds, &tags);
    print_items(builds, filters.format, list.links)?;
    println!();
  }

  if !repos.is_empty() {
    fix_tags(&mut repos, &tags);
    print_items(repos, filters.format, list.links)?;
    println!();
  }

  if !procedures.is_empty() {
    fix_tags(&mut procedures, &tags);
    print_items(procedures, filters.format, list.links)?;
    println!();
  }

  if !actions.is_empty() {
    fix_tags(&mut actions, &tags);
    print_items(actions, filters.format, list.links)?;
    println!();
  }

  if !syncs.is_empty() {
    fix_tags(&mut syncs, &tags);
    print_items(syncs, filters.format, list.links)?;
    println!();
  }

  Ok(())
}

async fn list_resources<T>(
  filters: &ResourceFilters,
  minimal: bool,
) -> anyhow::Result<()>
where
  T: ListResources,
  ResourceListItem<T::Info>: PrintTable + Serialize,
{
  let client = crate::command::komodo_client().await?;
  let (mut resources, tags) = tokio::try_join!(
    T::list(client, filters, minimal),
    client.read(ListTags::default()).map(|res| res.map(|res| res
      .into_iter()
      .map(|t| (t.id, t.name))
      .collect::<HashMap<_, _>>()))
  )?;
  fix_tags(&mut resources, &tags);
  if !resources.is_empty() {
    print_items(resources, filters.format, filters.links)?;
  }
  Ok(())
}

async fn list_schedules(
  filters: &ResourceFilters,
) -> anyhow::Result<()> {
  let client = crate::command::komodo_client().await?;
  let (mut schedules, tags) = tokio::try_join!(
    client
      .read(ListSchedules {
        tags: filters.tags.clone(),
        tag_behavior: Default::default(),
      })
      .map(|res| res.map(|res| res
        .into_iter()
        .filter(|s| s.next_scheduled_run.is_some())
        .collect::<Vec<_>>())),
    client.read(ListTags::default()).map(|res| res.map(|res| res
      .into_iter()
      .map(|t| (t.id, t.name))
      .collect::<HashMap<_, _>>()))
  )?;
  schedules.iter_mut().for_each(|resource| {
    resource.tags.iter_mut().for_each(|id| {
      let Some(name) = tags.get(id) else {
        *id = String::new();
        return;
      };
      id.clone_from(name);
    });
  });
  schedules.sort_by(|a, b| {
    match (a.next_scheduled_run, b.next_scheduled_run) {
      (Some(_), None) => return Ordering::Less,
      (None, Some(_)) => return Ordering::Greater,
      (Some(a), Some(b)) => return a.cmp(&b),
      (None, None) => {}
    }
    a.name.cmp(&b.name).then(a.enabled.cmp(&b.enabled))
  });
  if !schedules.is_empty() {
    print_items(schedules, filters.format, filters.links)?;
  }
  Ok(())
}

fn fix_tags<T>(
  resources: &mut [ResourceListItem<T>],
  tags: &HashMap<String, String>,
) {
  resources.iter_mut().for_each(|resource| {
    resource.tags.iter_mut().for_each(|id| {
      let Some(name) = tags.get(id) else {
        *id = String::new();
        return;
      };
      id.clone_from(name);
    });
  });
}

trait ListResources: Sized
where
  ResourceListItem<Self::Info>: PrintTable,
{
  type Info;
  async fn list(
    client: &KomodoClient,
    filters: &ResourceFilters,
    // For use with root `km ls`
    minimal: bool,
  ) -> anyhow::Result<Vec<ResourceListItem<Self::Info>>>;
}

// LIST

impl ListResources for ServerListItem {
  type Info = ServerListItemInfo;
  async fn list(
    client: &KomodoClient,
    filters: &ResourceFilters,
    _minimal: bool,
  ) -> anyhow::Result<Vec<Self>> {
    let servers = client
      .read(ListServers {
        query: ResourceQuery::builder()
          .tags(filters.tags.clone())
          // .tag_behavior(TagQueryBehavior::Any)
          .templates(filters.templates)
          .build(),
      })
      .await?;
    let names = parse_wildcards(&filters.names);
    let server_wildcards = parse_wildcards(&filters.servers);
    let mut servers = servers
      .into_iter()
      .filter(|server| {
        let state_check = if filters.all {
          true
        } else if filters.down {
          !matches!(server.info.state, ServerState::Ok)
        } else if filters.in_progress {
          false
        } else {
          matches!(server.info.state, ServerState::Ok)
        };
        let name_items = &[server.name.as_str()];
        state_check
          && matches_wildcards(&names, name_items)
          && matches_wildcards(&server_wildcards, name_items)
      })
      .collect::<Vec<_>>();
    servers.sort_by(|a, b| {
      a.info.state.cmp(&b.info.state).then(a.name.cmp(&b.name))
    });
    Ok(servers)
  }
}

impl ListResources for StackListItem {
  type Info = StackListItemInfo;
  async fn list(
    client: &KomodoClient,
    filters: &ResourceFilters,
    _minimal: bool,
  ) -> anyhow::Result<Vec<Self>> {
    let (servers, mut stacks) = tokio::try_join!(
      client
        .read(ListServers {
          query: ResourceQuery::builder().build(),
        })
        .map(|res| res.map(|res| res
          .into_iter()
          .map(|s| (s.id.clone(), s))
          .collect::<HashMap<_, _>>())),
      client.read(ListStacks {
        query: ResourceQuery::builder()
          .tags(filters.tags.clone())
          // .tag_behavior(TagQueryBehavior::Any)
          .templates(filters.templates)
          .build(),
      })
    )?;
    stacks.iter_mut().for_each(|stack| {
      if stack.info.server_id.is_empty() {
        return;
      }
      let Some(server) = servers.get(&stack.info.server_id) else {
        return;
      };
      stack.info.server_id.clone_from(&server.name);
    });
    let names = parse_wildcards(&filters.names);
    let servers = parse_wildcards(&filters.servers);
    let mut stacks = stacks
      .into_iter()
      .filter(|stack| {
        let state_check = if filters.all {
          true
        } else if filters.down {
          !matches!(
            stack.info.state,
            StackState::Running | StackState::Deploying
          )
        } else if filters.in_progress {
          matches!(stack.info.state, StackState::Deploying)
        } else {
          matches!(
            stack.info.state,
            StackState::Running | StackState::Deploying
          )
        };
        state_check
          && matches_wildcards(&names, &[stack.name.as_str()])
          && matches_wildcards(
            &servers,
            &[stack.info.server_id.as_str()],
          )
      })
      .collect::<Vec<_>>();
    stacks.sort_by(|a, b| {
      a.info
        .state
        .cmp(&b.info.state)
        .then(a.name.cmp(&b.name))
        .then(a.info.server_id.cmp(&b.info.server_id))
    });
    Ok(stacks)
  }
}

impl ListResources for DeploymentListItem {
  type Info = DeploymentListItemInfo;
  async fn list(
    client: &KomodoClient,
    filters: &ResourceFilters,
    _minimal: bool,
  ) -> anyhow::Result<Vec<Self>> {
    let (servers, mut deployments) = tokio::try_join!(
      client
        .read(ListServers {
          query: ResourceQuery::builder().build(),
        })
        .map(|res| res.map(|res| res
          .into_iter()
          .map(|s| (s.id.clone(), s))
          .collect::<HashMap<_, _>>())),
      client.read(ListDeployments {
        query: ResourceQuery::builder()
          .tags(filters.tags.clone())
          // .tag_behavior(TagQueryBehavior::Any)
          .templates(filters.templates)
          .build(),
      })
    )?;
    deployments.iter_mut().for_each(|deployment| {
      if deployment.info.server_id.is_empty() {
        return;
      }
      let Some(server) = servers.get(&deployment.info.server_id)
      else {
        return;
      };
      deployment.info.server_id.clone_from(&server.name);
    });
    let names = parse_wildcards(&filters.names);
    let servers = parse_wildcards(&filters.servers);
    let mut deployments = deployments
      .into_iter()
      .filter(|deployment| {
        let state_check = if filters.all {
          true
        } else if filters.down {
          !matches!(
            deployment.info.state,
            DeploymentState::Running | DeploymentState::Deploying
          )
        } else if filters.in_progress {
          matches!(deployment.info.state, DeploymentState::Deploying)
        } else {
          matches!(
            deployment.info.state,
            DeploymentState::Running | DeploymentState::Deploying
          )
        };
        state_check
          && matches_wildcards(&names, &[deployment.name.as_str()])
          && matches_wildcards(
            &servers,
            &[deployment.info.server_id.as_str()],
          )
      })
      .collect::<Vec<_>>();
    deployments.sort_by(|a, b| {
      a.info
        .state
        .cmp(&b.info.state)
        .then(a.name.cmp(&b.name))
        .then(a.info.server_id.cmp(&b.info.server_id))
    });
    Ok(deployments)
  }
}

impl ListResources for BuildListItem {
  type Info = BuildListItemInfo;
  async fn list(
    client: &KomodoClient,
    filters: &ResourceFilters,
    minimal: bool,
  ) -> anyhow::Result<Vec<Self>> {
    let (builders, mut builds) = tokio::try_join!(
      client
        .read(ListBuilders {
          query: ResourceQuery::builder().build(),
        })
        .map(|res| res.map(|res| res
          .into_iter()
          .map(|s| (s.id.clone(), s))
          .collect::<HashMap<_, _>>())),
      client.read(ListBuilds {
        query: ResourceQuery::builder()
          .tags(filters.tags.clone())
          // .tag_behavior(TagQueryBehavior::Any)
          .templates(filters.templates)
          .build(),
      })
    )?;
    builds.iter_mut().for_each(|build| {
      if build.info.builder_id.is_empty() {
        return;
      }
      let Some(builder) = builders.get(&build.info.builder_id) else {
        return;
      };
      build.info.builder_id.clone_from(&builder.name);
    });
    let names = parse_wildcards(&filters.names);
    let builders = parse_wildcards(&filters.builders);
    let mut builds = builds
      .into_iter()
      .filter(|build| {
        let state_check = if filters.all {
          true
        } else if filters.down {
          matches!(
            build.info.state,
            BuildState::Failed | BuildState::Unknown
          )
        } else if minimal || filters.in_progress {
          matches!(build.info.state, BuildState::Building)
        } else {
          true
        };
        state_check
          && matches_wildcards(&names, &[build.name.as_str()])
          && matches_wildcards(
            &builders,
            &[build.info.builder_id.as_str()],
          )
      })
      .collect::<Vec<_>>();
    builds.sort_by(|a, b| {
      a.name
        .cmp(&b.name)
        .then(a.info.builder_id.cmp(&b.info.builder_id))
        .then(a.info.state.cmp(&b.info.state))
    });
    Ok(builds)
  }
}

impl ListResources for RepoListItem {
  type Info = RepoListItemInfo;
  async fn list(
    client: &KomodoClient,
    filters: &ResourceFilters,
    minimal: bool,
  ) -> anyhow::Result<Vec<Self>> {
    let names = parse_wildcards(&filters.names);
    let mut repos = client
      .read(ListRepos {
        query: ResourceQuery::builder()
          .tags(filters.tags.clone())
          // .tag_behavior(TagQueryBehavior::Any)
          .templates(filters.templates)
          .build(),
      })
      .await?
      .into_iter()
      .filter(|repo| {
        let state_check = if filters.all {
          true
        } else if filters.down {
          matches!(
            repo.info.state,
            RepoState::Failed | RepoState::Unknown
          )
        } else if minimal || filters.in_progress {
          matches!(
            repo.info.state,
            RepoState::Building | RepoState::Cloning
          )
        } else {
          true
        };
        state_check
          && matches_wildcards(&names, &[repo.name.as_str()])
      })
      .collect::<Vec<_>>();
    repos.sort_by(|a, b| {
      a.name
        .cmp(&b.name)
        .then(a.info.server_id.cmp(&b.info.server_id))
        .then(a.info.builder_id.cmp(&b.info.builder_id))
    });
    Ok(repos)
  }
}

impl ListResources for ProcedureListItem {
  type Info = ProcedureListItemInfo;
  async fn list(
    client: &KomodoClient,
    filters: &ResourceFilters,
    minimal: bool,
  ) -> anyhow::Result<Vec<Self>> {
    let names = parse_wildcards(&filters.names);
    let mut procedures = client
      .read(ListProcedures {
        query: ResourceQuery::builder()
          .tags(filters.tags.clone())
          // .tag_behavior(TagQueryBehavior::Any)
          .templates(filters.templates)
          .build(),
      })
      .await?
      .into_iter()
      .filter(|procedure| {
        let state_check = if filters.all {
          true
        } else if filters.down {
          matches!(
            procedure.info.state,
            ProcedureState::Failed | ProcedureState::Unknown
          )
        } else if minimal || filters.in_progress {
          matches!(procedure.info.state, ProcedureState::Running)
        } else {
          true
        };
        state_check
          && matches_wildcards(&names, &[procedure.name.as_str()])
      })
      .collect::<Vec<_>>();
    procedures.sort_by(|a, b| {
      match (a.info.next_scheduled_run, b.info.next_scheduled_run) {
        (Some(_), None) => return Ordering::Less,
        (None, Some(_)) => return Ordering::Greater,
        (Some(a), Some(b)) => return a.cmp(&b),
        (None, None) => {}
      }
      a.name.cmp(&b.name).then(a.info.state.cmp(&b.info.state))
    });
    Ok(procedures)
  }
}

impl ListResources for ActionListItem {
  type Info = ActionListItemInfo;
  async fn list(
    client: &KomodoClient,
    filters: &ResourceFilters,
    minimal: bool,
  ) -> anyhow::Result<Vec<Self>> {
    let names = parse_wildcards(&filters.names);
    let mut actions = client
      .read(ListActions {
        query: ResourceQuery::builder()
          .tags(filters.tags.clone())
          // .tag_behavior(TagQueryBehavior::Any)
          .templates(filters.templates)
          .build(),
      })
      .await?
      .into_iter()
      .filter(|action| {
        let state_check = if filters.all {
          true
        } else if filters.down {
          matches!(
            action.info.state,
            ActionState::Failed | ActionState::Unknown
          )
        } else if minimal || filters.in_progress {
          matches!(action.info.state, ActionState::Running)
        } else {
          true
        };
        state_check
          && matches_wildcards(&names, &[action.name.as_str()])
      })
      .collect::<Vec<_>>();
    actions.sort_by(|a, b| {
      match (a.info.next_scheduled_run, b.info.next_scheduled_run) {
        (Some(_), None) => return Ordering::Less,
        (None, Some(_)) => return Ordering::Greater,
        (Some(a), Some(b)) => return a.cmp(&b),
        (None, None) => {}
      }
      a.name.cmp(&b.name).then(a.info.state.cmp(&b.info.state))
    });
    Ok(actions)
  }
}

impl ListResources for ResourceSyncListItem {
  type Info = ResourceSyncListItemInfo;
  async fn list(
    client: &KomodoClient,
    filters: &ResourceFilters,
    minimal: bool,
  ) -> anyhow::Result<Vec<Self>> {
    let names = parse_wildcards(&filters.names);
    let mut syncs = client
      .read(ListResourceSyncs {
        query: ResourceQuery::builder()
          .tags(filters.tags.clone())
          // .tag_behavior(TagQueryBehavior::Any)
          .templates(filters.templates)
          .build(),
      })
      .await?
      .into_iter()
      .filter(|sync| {
        let state_check = if filters.all {
          true
        } else if filters.down {
          matches!(
            sync.info.state,
            ResourceSyncState::Failed | ResourceSyncState::Unknown
          )
        } else if minimal || filters.in_progress {
          matches!(
            sync.info.state,
            ResourceSyncState::Syncing | ResourceSyncState::Pending
          )
        } else {
          true
        };
        state_check
          && matches_wildcards(&names, &[sync.name.as_str()])
      })
      .collect::<Vec<_>>();
    syncs.sort_by(|a, b| {
      a.name.cmp(&b.name).then(a.info.state.cmp(&b.info.state))
    });
    Ok(syncs)
  }
}

impl ListResources for BuilderListItem {
  type Info = BuilderListItemInfo;
  async fn list(
    client: &KomodoClient,
    filters: &ResourceFilters,
    minimal: bool,
  ) -> anyhow::Result<Vec<Self>> {
    let names = parse_wildcards(&filters.names);
    let mut builders = client
      .read(ListBuilders {
        query: ResourceQuery::builder()
          .tags(filters.tags.clone())
          // .tag_behavior(TagQueryBehavior::Any)
          .templates(filters.templates)
          .build(),
      })
      .await?
      .into_iter()
      .filter(|builder| {
        (!minimal || filters.all)
          && matches_wildcards(&names, &[builder.name.as_str()])
      })
      .collect::<Vec<_>>();
    builders.sort_by(|a, b| {
      a.name
        .cmp(&b.name)
        .then(a.info.builder_type.cmp(&b.info.builder_type))
    });
    Ok(builders)
  }
}

impl ListResources for AlerterListItem {
  type Info = AlerterListItemInfo;
  async fn list(
    client: &KomodoClient,
    filters: &ResourceFilters,
    minimal: bool,
  ) -> anyhow::Result<Vec<Self>> {
    let names = parse_wildcards(&filters.names);
    let mut syncs = client
      .read(ListAlerters {
        query: ResourceQuery::builder()
          .tags(filters.tags.clone())
          // .tag_behavior(TagQueryBehavior::Any)
          .templates(filters.templates)
          .build(),
      })
      .await?
      .into_iter()
      .filter(|sync| {
        (!minimal || filters.all)
          && matches_wildcards(&names, &[sync.name.as_str()])
      })
      .collect::<Vec<_>>();
    syncs.sort_by(|a, b| {
      a.info
        .enabled
        .cmp(&b.info.enabled)
        .then(a.name.cmp(&b.name))
        .then(a.info.endpoint_type.cmp(&b.info.endpoint_type))
    });
    Ok(syncs)
  }
}

// TABLE

impl PrintTable for ResourceListItem<ServerListItemInfo> {
  fn header(links: bool) -> &'static [&'static str] {
    if links {
      &["Server", "State", "Address", "Tags", "Link"]
    } else {
      &["Server", "State", "Address", "Tags"]
    }
  }
  fn row(self, links: bool) -> Vec<Cell> {
    let color = match self.info.state {
      ServerState::Ok => Color::Green,
      ServerState::NotOk => Color::Red,
      ServerState::Disabled => Color::Blue,
    };
    let mut res = vec![
      Cell::new(self.name).add_attribute(Attribute::Bold),
      Cell::new(self.info.state.to_string())
        .fg(color)
        .add_attribute(Attribute::Bold),
      Cell::new(self.info.address),
      Cell::new(self.tags.join(", ")),
    ];
    if links {
      res.push(Cell::new(resource_link(
        &cli_config().host,
        ResourceTargetVariant::Server,
        &self.id,
      )))
    }
    res
  }
}

impl PrintTable for ResourceListItem<StackListItemInfo> {
  fn header(links: bool) -> &'static [&'static str] {
    if links {
      &["Stack", "State", "Server", "Tags", "Link"]
    } else {
      &["Stack", "State", "Server", "Tags"]
    }
  }
  fn row(self, links: bool) -> Vec<comfy_table::Cell> {
    let color = match self.info.state {
      StackState::Down => Color::Blue,
      StackState::Running => Color::Green,
      StackState::Deploying => Color::DarkYellow,
      StackState::Paused => Color::DarkYellow,
      StackState::Unknown => Color::Magenta,
      _ => Color::Red,
    };
    // let source = if self.info.files_on_host {
    //   "On Host"
    // } else if !self.info.repo.is_empty() {
    //   self.info.repo_link.as_str()
    // } else {
    //   "UI Defined"
    // };
    let mut res = vec![
      Cell::new(self.name).add_attribute(Attribute::Bold),
      Cell::new(self.info.state.to_string())
        .fg(color)
        .add_attribute(Attribute::Bold),
      Cell::new(self.info.server_id),
      // Cell::new(source),
      Cell::new(self.tags.join(", ")),
    ];
    if links {
      res.push(Cell::new(resource_link(
        &cli_config().host,
        ResourceTargetVariant::Stack,
        &self.id,
      )))
    }
    res
  }
}

impl PrintTable for ResourceListItem<DeploymentListItemInfo> {
  fn header(links: bool) -> &'static [&'static str] {
    if links {
      &["Deployment", "State", "Server", "Tags", "Link"]
    } else {
      &["Deployment", "State", "Server", "Tags"]
    }
  }
  fn row(self, links: bool) -> Vec<comfy_table::Cell> {
    let color = match self.info.state {
      DeploymentState::NotDeployed => Color::Blue,
      DeploymentState::Running => Color::Green,
      DeploymentState::Deploying => Color::DarkYellow,
      DeploymentState::Paused => Color::DarkYellow,
      DeploymentState::Unknown => Color::Magenta,
      _ => Color::Red,
    };
    let mut res = vec![
      Cell::new(self.name).add_attribute(Attribute::Bold),
      Cell::new(self.info.state.to_string())
        .fg(color)
        .add_attribute(Attribute::Bold),
      Cell::new(self.info.server_id),
      Cell::new(self.tags.join(", ")),
    ];
    if links {
      res.push(Cell::new(resource_link(
        &cli_config().host,
        ResourceTargetVariant::Deployment,
        &self.id,
      )))
    }
    res
  }
}

impl PrintTable for ResourceListItem<BuildListItemInfo> {
  fn header(links: bool) -> &'static [&'static str] {
    if links {
      &["Build", "State", "Builder", "Tags", "Link"]
    } else {
      &["Build", "State", "Builder", "Tags"]
    }
  }
  fn row(self, links: bool) -> Vec<comfy_table::Cell> {
    let color = match self.info.state {
      BuildState::Ok => Color::Green,
      BuildState::Building => Color::DarkYellow,
      BuildState::Unknown => Color::Magenta,
      BuildState::Failed => Color::Red,
    };
    let mut res = vec![
      Cell::new(self.name).add_attribute(Attribute::Bold),
      Cell::new(self.info.state.to_string())
        .fg(color)
        .add_attribute(Attribute::Bold),
      Cell::new(self.info.builder_id),
      Cell::new(self.tags.join(", ")),
    ];
    if links {
      res.push(Cell::new(resource_link(
        &cli_config().host,
        ResourceTargetVariant::Build,
        &self.id,
      )));
    }
    res
  }
}

impl PrintTable for ResourceListItem<RepoListItemInfo> {
  fn header(links: bool) -> &'static [&'static str] {
    if links {
      &["Repo", "State", "Link", "Tags", "Link"]
    } else {
      &["Repo", "State", "Link", "Tags"]
    }
  }
  fn row(self, links: bool) -> Vec<comfy_table::Cell> {
    let color = match self.info.state {
      RepoState::Ok => Color::Green,
      RepoState::Building
      | RepoState::Cloning
      | RepoState::Pulling => Color::DarkYellow,
      RepoState::Unknown => Color::Magenta,
      RepoState::Failed => Color::Red,
    };
    let mut res = vec![
      Cell::new(self.name).add_attribute(Attribute::Bold),
      Cell::new(self.info.state.to_string())
        .fg(color)
        .add_attribute(Attribute::Bold),
      Cell::new(self.info.repo_link),
      Cell::new(self.tags.join(", ")),
    ];
    if links {
      res.push(Cell::new(resource_link(
        &cli_config().host,
        ResourceTargetVariant::Repo,
        &self.id,
      )))
    }
    res
  }
}

impl PrintTable for ResourceListItem<ProcedureListItemInfo> {
  fn header(links: bool) -> &'static [&'static str] {
    if links {
      &["Procedure", "State", "Next Run", "Tags", "Link"]
    } else {
      &["Procedure", "State", "Next Run", "Tags"]
    }
  }
  fn row(self, links: bool) -> Vec<comfy_table::Cell> {
    let color = match self.info.state {
      ProcedureState::Ok => Color::Green,
      ProcedureState::Running => Color::DarkYellow,
      ProcedureState::Unknown => Color::Magenta,
      ProcedureState::Failed => Color::Red,
    };
    let next_run = if let Some(ts) = self.info.next_scheduled_run {
      Cell::new(
        format_timetamp(ts)
          .unwrap_or(String::from("Invalid next ts")),
      )
      .add_attribute(Attribute::Bold)
    } else {
      Cell::new(String::from("None"))
    };
    let mut res = vec![
      Cell::new(self.name).add_attribute(Attribute::Bold),
      Cell::new(self.info.state.to_string())
        .fg(color)
        .add_attribute(Attribute::Bold),
      next_run,
      Cell::new(self.tags.join(", ")),
    ];
    if links {
      res.push(Cell::new(resource_link(
        &cli_config().host,
        ResourceTargetVariant::Procedure,
        &self.id,
      )))
    }
    res
  }
}

impl PrintTable for ResourceListItem<ActionListItemInfo> {
  fn header(links: bool) -> &'static [&'static str] {
    if links {
      &["Action", "State", "Next Run", "Tags", "Link"]
    } else {
      &["Action", "State", "Next Run", "Tags"]
    }
  }
  fn row(self, links: bool) -> Vec<comfy_table::Cell> {
    let color = match self.info.state {
      ActionState::Ok => Color::Green,
      ActionState::Running => Color::DarkYellow,
      ActionState::Unknown => Color::Magenta,
      ActionState::Failed => Color::Red,
    };
    let next_run = if let Some(ts) = self.info.next_scheduled_run {
      Cell::new(
        format_timetamp(ts)
          .unwrap_or(String::from("Invalid next ts")),
      )
      .add_attribute(Attribute::Bold)
    } else {
      Cell::new(String::from("None"))
    };
    let mut res = vec![
      Cell::new(self.name).add_attribute(Attribute::Bold),
      Cell::new(self.info.state.to_string())
        .fg(color)
        .add_attribute(Attribute::Bold),
      next_run,
      Cell::new(self.tags.join(", ")),
    ];
    if links {
      res.push(Cell::new(resource_link(
        &cli_config().host,
        ResourceTargetVariant::Action,
        &self.id,
      )));
    }
    res
  }
}

impl PrintTable for ResourceListItem<ResourceSyncListItemInfo> {
  fn header(links: bool) -> &'static [&'static str] {
    if links {
      &["Sync", "State", "Tags", "Link"]
    } else {
      &["Sync", "State", "Tags"]
    }
  }
  fn row(self, links: bool) -> Vec<comfy_table::Cell> {
    let color = match self.info.state {
      ResourceSyncState::Ok => Color::Green,
      ResourceSyncState::Pending | ResourceSyncState::Syncing => {
        Color::DarkYellow
      }
      ResourceSyncState::Unknown => Color::Magenta,
      ResourceSyncState::Failed => Color::Red,
    };
    let mut res = vec![
      Cell::new(self.name).add_attribute(Attribute::Bold),
      Cell::new(self.info.state.to_string())
        .fg(color)
        .add_attribute(Attribute::Bold),
      Cell::new(self.tags.join(", ")),
    ];
    if links {
      res.push(Cell::new(resource_link(
        &cli_config().host,
        ResourceTargetVariant::ResourceSync,
        &self.id,
      )))
    }
    res
  }
}

impl PrintTable for ResourceListItem<BuilderListItemInfo> {
  fn header(links: bool) -> &'static [&'static str] {
    if links {
      &["Builder", "Type", "Tags", "Link"]
    } else {
      &["Builder", "Type", "Tags"]
    }
  }
  fn row(self, links: bool) -> Vec<comfy_table::Cell> {
    let mut res = vec![
      Cell::new(self.name).add_attribute(Attribute::Bold),
      Cell::new(self.info.builder_type),
      Cell::new(self.tags.join(", ")),
    ];
    if links {
      res.push(Cell::new(resource_link(
        &cli_config().host,
        ResourceTargetVariant::Builder,
        &self.id,
      )));
    }
    res
  }
}

impl PrintTable for ResourceListItem<AlerterListItemInfo> {
  fn header(links: bool) -> &'static [&'static str] {
    if links {
      &["Alerter", "Type", "Enabled", "Tags", "Link"]
    } else {
      &["Alerter", "Type", "Enabled", "Tags"]
    }
  }
  fn row(self, links: bool) -> Vec<comfy_table::Cell> {
    let mut row = vec![
      Cell::new(self.name).add_attribute(Attribute::Bold),
      Cell::new(self.info.endpoint_type),
      if self.info.enabled {
        Cell::new(self.info.enabled.to_string()).fg(Color::Green)
      } else {
        Cell::new(self.info.enabled.to_string()).fg(Color::Red)
      },
      Cell::new(self.tags.join(", ")),
    ];
    if links {
      row.push(Cell::new(resource_link(
        &cli_config().host,
        ResourceTargetVariant::Alerter,
        &self.id,
      )));
    }
    row
  }
}

impl PrintTable for Schedule {
  fn header(links: bool) -> &'static [&'static str] {
    if links {
      &["Name", "Type", "Next Run", "Tags", "Link"]
    } else {
      &["Name", "Type", "Next Run", "Tags"]
    }
  }
  fn row(self, links: bool) -> Vec<comfy_table::Cell> {
    let next_run = if let Some(ts) = self.next_scheduled_run {
      Cell::new(
        format_timetamp(ts)
          .unwrap_or(String::from("Invalid next ts")),
      )
      .add_attribute(Attribute::Bold)
    } else {
      Cell::new(String::from("None"))
    };
    let (resource_type, id) = self.target.extract_variant_id();
    let mut res = vec![
      Cell::new(self.name).add_attribute(Attribute::Bold),
      Cell::new(self.target.extract_variant_id().0),
      next_run,
      Cell::new(self.tags.join(", ")),
    ];
    if links {
      res.push(Cell::new(resource_link(
        &cli_config().host,
        resource_type,
        id,
      )));
    }
    res
  }
}
