use fake::Dummy;
use fake::Fake;
use serde::Deserialize;
use serde::Serialize;
use serde_json::Value;

use crate::types::ResultDynError;

#[derive(Serialize, Deserialize, Debug)]
pub struct TaskFamily {
  pub parent_task: Task,
  pub children: Vec<TaskFamily>,
}

impl TaskFamily {
  pub fn json_string(task_families: &[TaskFamily]) -> ResultDynError<String> {
    return serde_json::to_string(task_families).map_err(failure::Error::from);
  }
}

#[derive(Clone, Debug, Serialize, Deserialize, Dummy)]
pub struct Task {
  pub id: String,
  pub task_type: String,
  pub phid: String,
  pub name: String,
  pub description: String,
  pub author_phid: String,
  pub assigned_phid: Option<String>,
  pub status: String,
  pub priority: String,
  pub point: Option<u64>,
  pub project_phids: Vec<String>,
  pub board: Option<Board>,
  pub created_at: u64,
  pub updated_at: u64,
}

impl Task {
  pub fn from_json(v: &Value) -> Task {
    let project_phids: &Vec<Value> = match &v["attachments"]["projects"]["projectPHIDs"] {
      Value::Array(arr) => arr,
      _ => panic!(
        "Project phids is not an array {:?}",
        v["attachments"]["projects"]["projectPHIDs"]
      ),
    };

    let project_phids: Vec<String> = project_phids.iter().map(json_to_string).collect();

    let board: Option<&Value> =
      Task::guess_board_from_projects(&v["attachments"]["columns"]["boards"], &project_phids);
    let fields: &Value = &v["fields"];

    let task = Task {
      id: format!("{}", v["id"].as_u64().unwrap()),
      task_type: json_to_string(&v["type"]),
      phid: json_to_string(&v["phid"]),
      name: json_to_string(&fields["name"]),
      description: json_to_string(&fields["description"]["raw"]),
      author_phid: json_to_string(&fields["authorPHID"]),
      assigned_phid: fields["ownerPHID"].as_str().map(Into::into),
      status: json_to_string(&fields["status"]["value"]),
      priority: json_to_string(&fields["priority"]["name"]),
      point: fields["points"].as_u64(),
      project_phids,
      board: board.map(|board: &Value| {
        return Board {
          id: board["id"].as_u64().unwrap(),
          phid: board["phid"].as_str().unwrap().into(),
          name: board["name"].as_str().unwrap().into(),
        };
      }),
      created_at: fields["dateCreated"].as_u64().unwrap(),
      updated_at: fields["dateModified"].as_u64().unwrap(),
    };

    return task;
  }

  pub fn guess_board_from_projects<'a>(
    boards: &'a Value,
    project_phids: &[String],
  ) -> Option<&'a Value> {
    return project_phids
      .iter()
      .find(|phid| {
        return boards[phid] != Value::Null;
      })
      .map(|phid| &boards[&phid]["columns"][0]);
  }
}

fn json_to_string(v: &Value) -> String {
  return v.as_str().unwrap().into();
}

#[derive(Clone, Debug, Serialize, Deserialize, Dummy)]
pub struct Board {
  pub id: u64,
  pub phid: String,
  pub name: String,
}

#[derive(Clone, Debug, Serialize, Deserialize, Dummy)]
pub struct Watchlist {
  pub id: Option<String>,
  pub name: String,
  pub tasks: Vec<Task>,
}

#[derive(Clone, Debug, Serialize, Deserialize, Dummy)]
pub struct User {
  pub id: String,
  pub phid: String,
  pub username: String,
  pub name: String,
  pub created_at: u64,
  pub updated_at: u64,
}

impl User {
  pub fn from_json(v: &Value) -> User {
    let fields: &Value = &v["fields"];

    return User {
      id: format!("{}", v["id"].as_u64().unwrap()),
      phid: json_to_string(&v["phid"]),
      username: json_to_string(&fields["username"]),
      name: json_to_string(&fields["realName"]),
      created_at: fields["dateCreated"].as_u64().unwrap(),
      updated_at: fields["dateModified"].as_u64().unwrap(),
    };
  }
}
