package api

import (
  "fmt"
  "os"
  "bytes"
  "bufio"
  "strings"
  "errors"
  "path/filepath"
  "net/http"
  "encoding/json"
  "io/ioutil"
  "io"
  "github.com/mitchellh/go-homedir"
  "dotty-cli/config"
)

//const host = "http://localhost:9500"
const host = "https://api.dotty.cloud"

func padRight(str, pad string, length int) string {
  if len(str) > length {
    return str
  }
  for {
    str += pad
    if len(str) > length {
      return str[0:length]
    }
  }
}

func makeRequest(req *http.Request, headers *map[string]string) (*http.Response, error){
  if headers != nil {
    for k := range *headers {
      req.Header.Set(k, (*headers)[k])
    }
  }
  client := &http.Client{}
  response, reqErr := client.Do(req)
  if reqErr != nil {
    return nil, reqErr
  }
  if response.StatusCode != 200 {
    buf := new(bytes.Buffer)
    buf.ReadFrom(response.Body)
    return nil, errors.New(fmt.Sprintf("%s. %s", response.Status, buf.String()))
  }
  return response, nil
}

func authenticatedRequest(token *string, method string, endpoint string, headers *map[string]string, data io.Reader) (*http.Response, error){
  var req *http.Request
  var err error
  if data == nil {
    req, err = http.NewRequest(method, host + endpoint, nil)
  } else {
    buf := new(bytes.Buffer)
    buf.ReadFrom(data)
    req, err = http.NewRequest(method, host + endpoint, buf)
  }
  if err != nil {
    return nil, err
  }
  req.Header.Add("Authorization", *token)
  return makeRequest(req, headers)
}

func unauthenticatedRequest(method string, endpoint string, headers *map[string]string, data io.Reader) (*http.Response, error){
  var req *http.Request
  var err error
  if data == nil {
    req, err = http.NewRequest(method, host + endpoint, nil)
  } else {
    buf := new(bytes.Buffer)
    buf.ReadFrom(data)
    req, err = http.NewRequest(method, host + endpoint, buf)
  }
  if err != nil {
    return nil, err
  }
  return makeRequest(req, headers)
}

func Add(key *string, filePath *string) error {
  conf := *config.Load()
  if _, existsErr := config.DotfileInfo(&conf, key) ; existsErr == nil {
    return errors.New("This key already exists in your local registry. Try running 'dotty push -k " + *key + "' instead.")
  }

  homeDir, _ := homedir.Dir()
  absPath, _ := filepath.Abs(*filePath)
  homeDirPath := strings.Replace(absPath, homeDir, "~", 1)
  thing, fileErr := os.Open(absPath)
  if fileErr != nil{
    return errors.New(fmt.Sprintf("Unable to read %s", absPath))
  }

  headers := map[string]string{"Key": *key, "Default-Path": homeDirPath, "Content-Type": "text/plain"};
  _, reqErr := authenticatedRequest(&conf.AccessToken, "POST", "/files", &headers, thing)
  if reqErr != nil {
    return reqErr
  }
  conf.Configs = append(conf.Configs, config.ConfigurationConfig{Key: *key, Path: homeDirPath})
  config.Store(&conf)
  return nil
}

func Remove(key *string) error {
  conf := *config.Load()
  index := -1
  for i, localConfig := range conf.Configs {
    if localConfig.Key == *key {
      index = i
      break
    }
  }
  if index > -1 {
    conf.Configs = append(conf.Configs[:index], conf.Configs[index+1:]...)
  }
  _, reqErr := authenticatedRequest(&conf.AccessToken, "DELETE", "/files/" + *key, nil, nil)
  if reqErr != nil {
    return reqErr
  }
  config.Store(&conf)
  return nil
}

func Push(key *string) error {
  conf := *config.Load()
  fileConfig, findErr := config.DotfileInfo(&conf, key)
  if findErr != nil {
    errors.New("This key does not yet exist in your local configuration. Do you need to run 'dotty update' first?")
  }
  expandedPath, _ := homedir.Expand(fileConfig.Path)
  fileBody, fileErr := os.Open(expandedPath)
  if fileErr != nil {
    errors.New(fmt.Sprintf("%s is a known key but %s cannot be opened. You may need to edit the confsync configuration directly.", key, fileConfig.Path))
  }
  headers := map[string]string{"Content-Type": "text/plain"}
  _, reqErr := authenticatedRequest(&conf.AccessToken, "PUT", "/files/" + *key, &headers, fileBody)
  if reqErr != nil {
    return reqErr
  }
  return nil
}

func Pull(key *string) error {
  conf := *config.Load()
  headers := map[string]string{"Key": *key};
  res, reqErr := authenticatedRequest(&conf.AccessToken, "GET", "/files/" + *key, &headers, nil)
  if reqErr != nil {
    return reqErr
  }
  bodyBytes, _ := ioutil.ReadAll(res.Body)
  path := res.Header.Get("Default-Path")
  configInfo, existsErr := config.DotfileInfo(&conf, key)
  if existsErr != nil {
    fmt.Printf("%s is a new key.\n", *key)
    reader := bufio.NewReader(os.Stdin)
    fmt.Printf("Enter a path for this file on this machine (%s): ", path)
    newPath, _ := reader.ReadString('\n')
    newPath = strings.TrimSpace(newPath)
    if newPath != "" {
      if strings.HasPrefix(newPath, "~") {
        path = newPath
      } else{
        homeDir, _ := homedir.Dir()
        absPath, _ := filepath.Abs(newPath)
        path = strings.Replace(absPath, homeDir, "~", 1)
      }
    }
    conf.Configs = append(conf.Configs, config.ConfigurationConfig{Key: *key, Path: path})
  } else {
    path = configInfo.Path
  }
  writePath, _ := homedir.Expand(path)
  dirErr := os.MkdirAll(filepath.Dir(writePath), 0777)
  if dirErr != nil {
    return dirErr
  }
  writeErr := ioutil.WriteFile(writePath, bodyBytes, 0644)
  if writeErr != nil{
    fmt.Printf("Unable to write file\n\nMaybe one of these will help:\n- Manually editing the dotty configuration file to select a new local dir.\n- Re-running as an administrator.")
    return writeErr
  }
  config.Store(&conf)
  return nil
}

func Update() error {
  conf := *config.Load()
  res, reqErr := authenticatedRequest(&conf.AccessToken, "GET", "/files", nil, nil)
  if reqErr != nil {
    return reqErr
  }

  bodyBytes, _ := ioutil.ReadAll(res.Body)
  type updateResponse struct {
    Key string `json:"key"`
    DefaultPath string `json:"defaultPath"`
  }
  u := []updateResponse{}
  json.Unmarshal(bodyBytes, &u)

  for _, configFile := range u {
    _, confErr := config.DotfileInfo(&conf, &configFile.Key)
    if confErr != nil{
      reader := bufio.NewReader(os.Stdin)
      fmt.Printf("New file found: %s.\nEnter local path for the file (%s): ", configFile.Key, configFile.DefaultPath)
      newPath, _ := reader.ReadString('\n')
      newPath = strings.TrimSpace(newPath)
      if newPath != "" {
        if strings.HasPrefix(newPath, "~") {
          configFile.DefaultPath = newPath
        } else {
          homeDir, _ := homedir.Dir()
          absPath, _ := filepath.Abs(newPath)
          configFile.DefaultPath = strings.Replace(absPath, homeDir, "~", 1)
        }
      }
      conf.Configs = append(conf.Configs, config.ConfigurationConfig{Key: strings.TrimSpace(configFile.Key), Path: configFile.DefaultPath})
    }
  }
  config.Store(&conf)
  return nil
}

func List() error {
  conf := *config.Load()
  res, reqErr := authenticatedRequest(&conf.AccessToken, "GET", "/files", nil, nil)
  if reqErr != nil {
    return reqErr
  }
  bodyBytes, _ := ioutil.ReadAll(res.Body)
  type updateResponse struct {
    Key string `json:"key"`
    DefaultPath string `json:"defaultPath"`
  }
  u := []updateResponse{}
  json.Unmarshal(bodyBytes, &u)
  fmt.Printf("The following files are tracked by Dotty:\n\n")
  notFounds := 0
  for _, configFile := range u {
    localConfig, localErr := config.DotfileInfo(&conf, &configFile.Key)
    fmt.Printf("%s\t(default: %s", padRight(configFile.Key, " ", 20), configFile.DefaultPath)
    if localErr == nil {
      fmt.Printf(" local: %s", localConfig.Path)
    } else {
      notFounds++;
    }
    fmt.Printf(")\n")
  }
  if notFounds > 0 {
    fmt.Printf("\n")
    fmt.Printf("INFO: You have files in your account not registered locally. Run 'dotty update' to sync file list.")
  }
  return nil
}

func Login(email *string, password *string) error {
  m := make(map[string]string)
  m["email"] = *email
  m["password"] = *password

  data, _ := json.Marshal(m)
  res, reqErr := http.Post(host + "/tokens", "application/json", strings.NewReader(string(data)))
  if reqErr != nil{
    return reqErr
  }
  buf := new(bytes.Buffer)
  buf.ReadFrom(res.Body)
  if res.StatusCode == 200 {
    conf := *config.Load()
    conf.AccessToken = buf.String()
    config.Store(&conf)
    return nil
  }
  return errors.New(buf.String())
}

func Register(email *string, password *string) error {
  m := make(map[string]string)
  m["email"] = *email
  m["password"] = *password

  data, _ := json.Marshal(m)
  res, reqErr := http.Post(host + "/accounts", "application/json", strings.NewReader(string(data)))
  if reqErr != nil{
    return reqErr
  }
  buf := new(bytes.Buffer)
  buf.ReadFrom(res.Body)
  if res.StatusCode == 200 {
    conf := *config.Load()
    conf.AccessToken = buf.String()
    config.Store(&conf)
    return nil
  }
  return errors.New(buf.String())
}

func Logout(arg string) error {
  conf := *config.Load()
  var reqErr error
  if arg == "all" {
    _, reqErr = authenticatedRequest(&conf.AccessToken, "DELETE", "/tokens/all", nil, nil)
  } else if arg == "others" {
    _, reqErr = authenticatedRequest(&conf.AccessToken, "DELETE", "/tokens/others", nil, nil)
  } else {
    _, reqErr = authenticatedRequest(&conf.AccessToken, "DELETE", "/tokens", nil, nil)
  }
  if reqErr == nil && arg != "others" {
    conf.AccessToken = ""
    config.Store(&conf)
  }
  return reqErr
}

func ChangePassword(currentPassword *string, newPassword *string) error {
  m := make(map[string]string)
  m["currentPassword"] = *currentPassword
  m["newPassword"] = *newPassword
  data, _ := json.Marshal(m)
  conf := *config.Load()
  _, reqErr := authenticatedRequest(&conf.AccessToken, "PUT", "/accounts/password", nil, strings.NewReader(string(data)))
  return reqErr
}

func SendPasswordResetEmail(email *string) error {
  _, reqErr := unauthenticatedRequest("POST", "/accounts/password/tokens", nil, strings.NewReader(*email))
  return reqErr
}

func ResetPassword(token *string, password *string) error {
  m := make(map[string]string)
  m["password"] = *password
  m["token"] = *token
  data, _ := json.Marshal(m)

  headers := map[string]string{"Content-Type": "application/json"};
  _, reqErr := unauthenticatedRequest("PUT", "/accounts/password/tokens", &headers, strings.NewReader(string(data)))
  return reqErr
}
