package main

// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License.  You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied.  See the License for the
// specific language governing permissions and limitations
// under the License.

import (
	"encoding/json"
	"errors"
	"flag"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"regexp"
	"strconv"
	"strings"
	"time"

	log "github.com/apache/trafficcontrol/v8/lib/go-log"
	tc "github.com/apache/trafficcontrol/v8/lib/go-tc"
	client "github.com/apache/trafficcontrol/v8/traffic_ops/v5-client"

	"github.com/fsnotify/fsnotify"
	"github.com/kelseyhightower/envconfig"
)

var startedFile = "enroller-started"

type session struct {
	*client.Session
}

func newSession(reqTimeout time.Duration, toURL string, toUser string, toPass string) (session, error) {
	s, _, err := client.LoginWithAgent(toURL, toUser, toPass, true, "cdn-in-a-box-enroller", true, reqTimeout)
	return session{s}, err
}

func (s session) getParameter(m tc.ParameterV5, header http.Header) (tc.ParameterV5, error) {
	// TODO: s.GetParameterByxxx() does not seem to work with values with spaces --
	// doing this the hard way for now
	opts := client.RequestOptions{Header: header}
	parameters, _, err := s.GetParameters(opts)
	if err != nil {
		return m, fmt.Errorf("getting Parameters: %v - alerts: %+v", err, parameters.Alerts)
	}
	for _, p := range parameters.Response {
		if p.Name == m.Name && p.Value == m.Value && p.ConfigFile == m.ConfigFile {
			return p, nil
		}
	}
	return m, fmt.Errorf("no parameter matching name %s, configFile %s, value %s", m.Name, m.ConfigFile, m.Value)
}

// enrollType takes a json file and creates a Type object using the TO API
func enrollType(toSession *session, r io.Reader) error {
	dec := json.NewDecoder(r)
	var s tc.TypeV5
	err := dec.Decode(&s)
	if err != nil {
		log.Infof("error decoding Type: %s", err)
		return err
	}

	alerts, _, err := toSession.CreateType(s, client.RequestOptions{})
	if err != nil {
		for _, alert := range alerts.Alerts {
			if alert.Level == tc.ErrorLevel.String() && strings.Contains(alert.Text, "already exists") {
				log.Infof("Type '%s' already exists", s.Name)
				return nil
			}
		}
		err = fmt.Errorf("error creating Type: %v - alerts: %+v", err, alerts.Alerts)
		log.Infoln(err)
		return err
	}

	enc := json.NewEncoder(os.Stdout)
	enc.SetIndent("", "  ")
	err = enc.Encode(&alerts)

	return err
}

// enrollCDN takes a json file and creates a CDN object using the TO API
func enrollCDN(toSession *session, r io.Reader) error {
	dec := json.NewDecoder(r)
	var s tc.CDNV5
	err := dec.Decode(&s)
	if err != nil {
		log.Infof("error decoding CDN: %v", err)
		return err
	}

	alerts, _, err := toSession.CreateCDN(s, client.RequestOptions{})
	if err != nil {
		for _, alert := range alerts.Alerts {
			if strings.Contains(alert.Text, "already exists") {
				log.Infof("CDN '%s' already exists", s.Name)
				return nil
			}
		}
		log.Infof("error creating CDN: %v - alerts: %+v", err, alerts.Alerts)
		return err
	}

	enc := json.NewEncoder(os.Stdout)
	enc.SetIndent("", "  ")
	err = enc.Encode(&alerts)

	return err
}

func enrollASN(toSession *session, r io.Reader) error {
	dec := json.NewDecoder(r)
	var s tc.ASNV5
	err := dec.Decode(&s)
	if err != nil {
		log.Infof("error decoding ASN: %s\n", err)
		return err
	}

	alerts, _, err := toSession.CreateASN(s, client.RequestOptions{})
	if err != nil {
		for _, alert := range alerts.Alerts {
			if strings.Contains(alert.Text, "already exists") {
				log.Infof("asn %d already exists", s.ASN)
				return nil
			}
		}
		err = fmt.Errorf("error creating ASN: %s - alerts: %+v", err, alerts.Alerts)
		log.Infoln(err)
		return err
	}

	enc := json.NewEncoder(os.Stdout)
	enc.SetIndent("", "  ")
	err = enc.Encode(&alerts)

	return err
}

// enrollCachegroup takes a json file and creates a Cachegroup object using the TO API
func enrollCachegroup(toSession *session, r io.Reader) error {
	dec := json.NewDecoder(r)
	var s tc.CacheGroupNullableV5
	err := dec.Decode(&s)
	if err != nil {
		log.Infof("error decoding Cache Group: '%s'", err)
		return err
	}

	alerts, _, err := toSession.CreateCacheGroup(s, client.RequestOptions{})
	if err != nil {
		for _, alert := range alerts.Alerts.Alerts {
			if strings.Contains(alert.Text, "already exists") {
				log.Infof("Cache Group '%s' already exists", *s.Name)
				return nil
			}
		}
		err = fmt.Errorf("error creating Cache Group: %v - alerts: %+v", err, alerts.Alerts)
		log.Infoln(err)
		return err
	}

	enc := json.NewEncoder(os.Stdout)
	enc.SetIndent("", "  ")
	err = enc.Encode(&alerts)

	return err
}

func enrollTopology(toSession *session, r io.Reader) error {
	dec := json.NewDecoder(r)
	var s tc.TopologyV5
	err := dec.Decode(&s)
	if err != nil && err != io.EOF {
		log.Infof("error decoding Topology: %s", err)
		return err
	}

	alerts, _, err := toSession.CreateTopology(s, client.RequestOptions{})
	if err != nil {
		for _, alert := range alerts.Alerts.Alerts {
			if alert.Level == tc.ErrorLevel.String() && strings.Contains(alert.Text, "already exists") {
				log.Infof("topology %s already exists", s.Name)
				return nil
			}
		}
		err = fmt.Errorf("error creating Topology: %v - alerts: %+v", err, alerts.Alerts.Alerts)
		log.Infoln(err)
		return err
	}

	enc := json.NewEncoder(os.Stdout)
	enc.SetIndent("", "  ")
	err = enc.Encode(&alerts)

	return err
}

func enrollDeliveryService(toSession *session, r io.Reader) error {
	dec := json.NewDecoder(r)
	var s tc.DeliveryServiceV5
	err := dec.Decode(&s)
	if err != nil {
		log.Infof("error decoding DeliveryService: %v", err)
		return err
	}

	alerts, _, err := toSession.CreateDeliveryService(s, client.RequestOptions{})
	if err != nil {
		for _, alert := range alerts.Alerts.Alerts {
			if strings.Contains(alert.Text, "already exists") {
				log.Infof("Delivery Service '%s' already exists", s.XMLID)
				return nil
			}
		}
		log.Infof("error creating Delivery Service: %v - alerts: %+v", err, alerts.Alerts)
		return err
	}

	enc := json.NewEncoder(os.Stdout)
	enc.SetIndent("", "  ")
	err = enc.Encode(&alerts)

	return err
}

// enrollDeliveryServicesRequiredCapability takes a json file and creates a DeliveryServicesRequiredCapability object using the TO API
func enrollDeliveryServicesRequiredCapability(toSession *session, r io.Reader) error {
	dec := json.NewDecoder(r)
	var dsrc tc.DeliveryServicesRequiredCapability
	err := dec.Decode(&dsrc)
	if err != nil {
		log.Infof("error decoding Delivery Services Required Capability: %s\n", err)
		return err
	}

	if dsrc.XMLID == nil {
		return errors.New("required capability had no XMLID")
	}

	opts := client.NewRequestOptions()
	opts.QueryParameters.Set("xmlId", *dsrc.XMLID)
	dses, _, err := toSession.GetDeliveryServices(opts)
	if err != nil {
		log.Infof("getting Delivery Service by XMLID %s: %s", *dsrc.XMLID, err.Error())
		return err
	}
	if len(dses.Response) < 1 {
		err = fmt.Errorf("could not find a Delivey Service with XMLID %s", *dsrc.XMLID)
		log.Infoln(err)
		return err
	}
	dsrc.DeliveryServiceID = dses.Response[0].ID

	dsUpdate := dses.Response[0]
	dsUpdate.RequiredCapabilities = []string{*dsrc.RequiredCapability}

	sc := tc.ServerCapabilityV5{
		Name:        *dsrc.RequiredCapability,
		Description: "description",
	}

	_, _, err = toSession.CreateServerCapability(sc, client.RequestOptions{})
	if err != nil {
		log.Infof("error creating Server Capability: %v", err)
		return err
	}

	_, _, err = toSession.UpdateDeliveryService(*dsUpdate.ID, dsUpdate, client.RequestOptions{})
	if err != nil {
		log.Infof("error creating Delivery Services Required Capability: %v", err)
		return err
	}
	return err
}

func enrollDeliveryServiceServer(toSession *session, r io.Reader) error {
	dec := json.NewDecoder(r)

	// DeliveryServiceServers lists ds xmlid and array of server names.  Use that to create multiple DeliveryServiceServer objects
	var dss tc.DeliveryServiceServers
	err := dec.Decode(&dss)
	if err != nil {
		log.Infof("error decoding DeliveryServiceServer: %s\n", err)
		return err
	}

	opts := client.RequestOptions{QueryParameters: url.Values{"xmlId": []string{dss.XmlId}}}
	dses, _, err := toSession.GetDeliveryServices(opts)
	if err != nil {
		return err
	}
	if len(dses.Response) == 0 {
		return errors.New("no deliveryservice with name " + dss.XmlId)
	}
	if dses.Response[0].ID == nil {
		return errors.New("Deliveryservice with name " + dss.XmlId + " has a nil ID")
	}
	dsID := *dses.Response[0].ID

	opts.QueryParameters = url.Values{}
	var serverIDs []int
	for _, sn := range dss.ServerNames {
		opts.QueryParameters.Set("hostName", sn)
		servers, _, err := toSession.GetServers(opts)
		if err != nil {
			return err
		}
		if len(servers.Response) == 0 {
			return errors.New("no server with hostName " + sn)
		}
		serverIDs = append(serverIDs, servers.Response[0].ID)
	}
	resp, _, err := toSession.CreateDeliveryServiceServers(dsID, serverIDs, true, client.RequestOptions{})
	if err != nil {
		log.Infof("error assigning servers %v to Delivery Service #%d: %v - alerts: %+v", serverIDs, dsID, err, resp.Alerts)
	}

	return err
}

func enrollDivision(toSession *session, r io.Reader) error {
	dec := json.NewDecoder(r)
	var s tc.DivisionV5
	err := dec.Decode(&s)
	if err != nil {
		log.Infof("error decoding Division: %s", err)
		return err
	}

	alerts, _, err := toSession.CreateDivision(s, client.RequestOptions{})
	if err != nil {
		for _, alert := range alerts.Alerts {
			if strings.Contains(alert.Text, "already exists") {
				log.Infof("division %s already exists", s.Name)
				return nil
			}
		}
		log.Infof("error creating Division: %v - alerts: %+v", err, alerts.Alerts)
		return err
	}

	enc := json.NewEncoder(os.Stdout)
	enc.SetIndent("", "  ")
	err = enc.Encode(&alerts)

	return err
}

func enrollOrigin(toSession *session, r io.Reader) error {
	dec := json.NewDecoder(r)
	var s tc.OriginV5
	err := dec.Decode(&s)
	if err != nil {
		log.Infof("error decoding Origin: %v", err)
		return err
	}

	alerts, _, err := toSession.CreateOrigin(s, client.RequestOptions{})
	if err != nil {
		for _, alert := range alerts.Alerts.Alerts {
			if alert.Level == tc.ErrorLevel.String() && strings.Contains(alert.Text, "already exists") {
				log.Infof("Origin '%s' already exists", s.Name)
				return nil
			}
		}
		log.Infof("error creating Origin: %v - alerts: %+v", err, alerts.Alerts)
		return err
	}

	enc := json.NewEncoder(os.Stdout)
	enc.SetIndent("", "  ")
	err = enc.Encode(&alerts)

	return err
}

func enrollParameter(toSession *session, r io.Reader) error {
	dec := json.NewDecoder(r)
	var params []tc.ParameterV5
	err := dec.Decode(&params)
	if err != nil {
		log.Infof("error decoding Parameter: %s\n", err)
		return err
	}

	for _, p := range params {
		eparam, err := toSession.getParameter(p, nil)
		var alerts tc.Alerts
		if err == nil {
			// existing param -- update
			alerts, _, err = toSession.UpdateParameter(eparam.ID, p, client.RequestOptions{})
			if err != nil {
				log.Infof("error updating parameter %d: %v with %+v - alerts: %+v ", eparam.ID, err, p, alerts.Alerts)
				break
			}
		} else {
			alerts, _, err = toSession.CreateParameter(p, client.RequestOptions{})
			if err != nil {
				log.Infof("error creating parameter: %v from %+v - alerts: %+v", err, p, alerts.Alerts)
				return err
			}
			eparam, err = toSession.getParameter(p, nil)
			if err != nil {
				return err
			}
		}

		// link parameter with profiles
		if len(p.Profiles) > 0 {
			var profiles []string
			err = json.Unmarshal(p.Profiles, &profiles)
			if err != nil {
				log.Infof("%v", err)
				return err
			}

			opts := client.NewRequestOptions()
			for _, n := range profiles {
				opts.QueryParameters.Set("name", n)
				profiles, _, err := toSession.GetProfiles(opts)
				if err != nil {
					return err
				}
				if len(profiles.Response) == 0 {
					return errors.New("no profile with name " + n)
				}

				pp := tc.ProfileParameterCreationRequest{ParameterID: eparam.ID, ProfileID: profiles.Response[0].ID}
				resp, _, err := toSession.CreateProfileParameter(pp, client.RequestOptions{})
				if err != nil {
					found := false
					for _, alert := range resp.Alerts {
						if alert.Level == tc.ErrorLevel.String() && strings.Contains(alert.Text, "already exists") {
							found = true
							break
						}
					}
					if found {
						continue
					}
					// the original code didn't actually do anything if the error wasn't that the
					// Profile/Parameter association already exists.
					// TODO: handle other errors?
				}
			}
		}
		enc := json.NewEncoder(os.Stdout)
		enc.SetIndent("", "  ")
		err = enc.Encode(&alerts)
	}
	return err
}

func enrollPhysLocation(toSession *session, r io.Reader) error {
	dec := json.NewDecoder(r)
	var s tc.PhysLocationV5
	err := dec.Decode(&s)
	if err != nil {
		err = fmt.Errorf("error decoding Physical Location: %v", err)
		log.Infoln(err)
		return err
	}

	alerts, _, err := toSession.CreatePhysLocation(s, client.RequestOptions{})
	if err != nil {
		for _, alert := range alerts.Alerts {
			if alert.Level == tc.ErrorLevel.String() && strings.Contains(alert.Text, "already exists") {
				log.Infof("Physical Location %s already exists", s.Name)
				return nil
			}

		}
		err = fmt.Errorf("error creating Physical Location '%s': %v - alerts: %+v", s.Name, err, alerts.Alerts)
		log.Infoln(err)
		return err
	}

	enc := json.NewEncoder(os.Stdout)
	enc.SetIndent("", "  ")
	err = enc.Encode(&alerts)

	return err
}

func enrollRegion(toSession *session, r io.Reader) error {
	dec := json.NewDecoder(r)
	var s tc.RegionV5
	err := dec.Decode(&s)
	if err != nil {
		log.Infof("error decoding Region: %s\n", err)
		return err
	}

	alerts, _, err := toSession.CreateRegion(s, client.RequestOptions{})
	if err != nil {
		for _, alert := range alerts.Alerts {
			if alert.Level == tc.ErrorLevel.String() && strings.Contains(alert.Text, "already exists") {
				log.Infof("a Region named '%s' already exists", s.Name)
				return nil
			}
		}
		err = fmt.Errorf("error creating Region '%s': %v - alerts: %+v", s.Name, err, alerts.Alerts)
		log.Infoln(err)
		return err
	}

	enc := json.NewEncoder(os.Stdout)
	enc.SetIndent("", "  ")
	err = enc.Encode(&alerts)

	return err
}

func enrollStatus(toSession *session, r io.Reader) error {
	dec := json.NewDecoder(r)
	var s tc.StatusV5
	err := dec.Decode(&s)
	if err != nil {
		log.Infof("error decoding Status: %s", err)
		return err
	}

	alerts, _, err := toSession.CreateStatus(s, client.RequestOptions{})
	if err != nil {
		for _, alert := range alerts.Alerts {
			if alert.Level == tc.ErrorLevel.String() && strings.Contains(alert.Text, "already exists") {
				log.Infof("status %s already exists", *s.Name)
				return nil
			}
		}
		err = fmt.Errorf("error creating Status: %v - alerts: %+v", err, alerts.Alerts)
		return err
	}

	enc := json.NewEncoder(os.Stdout)
	enc.SetIndent("", "  ")
	err = enc.Encode(&alerts)

	return err
}

func enrollTenant(toSession *session, r io.Reader) error {
	dec := json.NewDecoder(r)
	var s tc.TenantV5
	err := dec.Decode(&s)
	if err != nil {
		log.Infof("error decoding Tenant: %s", err)
		return err
	}

	alerts, _, err := toSession.CreateTenant(s, client.RequestOptions{})
	if err != nil {
		for _, alert := range alerts.Alerts.Alerts {
			if alert.Level == tc.ErrorLevel.String() && strings.Contains(alert.Text, "already exists") {
				log.Infof("tenant %s already exists", *s.Name)
				return nil
			}
		}
		err = fmt.Errorf("error creating Tenant: %v - alerts: %+v", err, alerts.Alerts)
		log.Infoln(err)
		return err
	}

	enc := json.NewEncoder(os.Stdout)
	enc.SetIndent("", "  ")
	err = enc.Encode(&alerts)

	return err
}

func enrollUser(toSession *session, r io.Reader) error {
	dec := json.NewDecoder(r)
	var s tc.UserV4
	err := dec.Decode(&s)
	log.Infof("User is %++v\n", s)
	if err != nil {
		log.Infof("error decoding User: %v", err)
		return err
	}

	alerts, _, err := toSession.CreateUser(s, client.RequestOptions{})
	if err != nil {
		for _, alert := range alerts.Alerts.Alerts {
			if alert.Level == tc.ErrorLevel.String() && strings.Contains(alert.Text, "already exists") {
				log.Infof("user %s already exists\n", s.Username)
				return nil
			}
		}
		err = fmt.Errorf("error creating User: %v - alerts: %+v", err, alerts.Alerts)
		log.Infoln(err)
		return err
	}

	enc := json.NewEncoder(os.Stdout)
	enc.SetIndent("", "  ")
	err = enc.Encode(&alerts)

	return err
}

// enrollProfile takes a json file and creates a Profile object using the TO API
func enrollProfile(toSession *session, r io.Reader) error {
	dec := json.NewDecoder(r)
	var profile tc.ProfileV5

	err := dec.Decode(&profile)
	if err != nil {
		log.Infof("error decoding Profile: %s\n", err)
		return err
	}
	// get a copy of the parameters
	parameters := profile.Parameters

	enc := json.NewEncoder(os.Stdout)
	enc.SetIndent("  ", "")
	enc.Encode(profile)

	if len(profile.Name) == 0 {
		log.Infoln("missing name on profile")
		return errors.New("missing name on profile")
	}

	opts := client.NewRequestOptions()
	opts.QueryParameters.Set("name", profile.Name)
	profiles, _, err := toSession.GetProfiles(opts)

	createProfile := false
	if err != nil || len(profiles.Response) == 0 {
		// no profile by that name -- need to create it
		createProfile = true
	} else {
		// updating - ID needs to match
		profile = profiles.Response[0]
	}

	var alerts tc.Alerts
	var action string
	if createProfile {
		alerts, _, err = toSession.CreateProfile(profile, client.RequestOptions{})
		if err != nil {
			found := false
			for _, alert := range alerts.Alerts {
				if alert.Level == tc.ErrorLevel.String() && strings.Contains(alert.Text, "already exists") {
					found = true
					break
				}
			}
			if found {
				log.Infof("profile %s already exists", profile.Name)
			} else {
				log.Infof("error creating profile from %+v: %v - alerts: %+v", profile, err, alerts.Alerts)
			}
		}
		profiles, _, err = toSession.GetProfiles(opts)
		if err != nil {
			log.Infof("error getting profile ID from %+v: %v - alerts: %+v", profile, err, profiles.Alerts)
		}
		if len(profiles.Response) == 0 {
			err = fmt.Errorf("no results returned for getting profile ID from %+v", profile)
			log.Infoln(err)
			return err
		}
		profile = profiles.Response[0]
		action = "creating"
	} else {
		alerts, _, err = toSession.UpdateProfile(profile.ID, profile, client.RequestOptions{})
		action = "updating"
	}

	if err != nil {
		log.Infof("error "+action+" from %s: %s", err)
		return err
	}

	for _, p := range parameters {
		var name, configFile, value string
		var secure bool
		if p.ConfigFile != nil {
			configFile = *p.ConfigFile
		}
		if p.Name != nil {
			name = *p.Name
		}
		if p.Value != nil {
			value = *p.Value
		}
		param := tc.ParameterV5{ConfigFile: configFile, Name: name, Value: value, Secure: secure}
		eparam, err := toSession.getParameter(param, nil)
		if err != nil {
			// create it
			log.Infof("creating param %+v", param)
			newAlerts, _, err := toSession.CreateParameter(param, client.RequestOptions{})
			if err != nil {
				log.Infof("can't create parameter %+v: %s, %v", param, err, newAlerts.Alerts)
				continue
			}
			eparam, err = toSession.getParameter(param, nil)
			if err != nil {
				log.Infof("error getting new parameter %+v: \n", param)
				log.Infof(err.Error())
				continue
			}
		} else {
			log.Infof("found param %+v\n", eparam)
		}

		if eparam.ID < 1 {
			log.Infof("param ID not found for %v", eparam)
			continue
		}
		pp := tc.ProfileParameterCreationRequest{ProfileID: profile.ID, ParameterID: eparam.ID}
		resp, _, err := toSession.CreateProfileParameter(pp, client.RequestOptions{})
		if err != nil {
			found := false
			for _, alert := range resp.Alerts {
				if alert.Level == tc.ErrorLevel.String() && strings.Contains(alert.Text, "already exists") {
					found = true
					break
				}
			}
			if !found {
				log.Infof("error creating profileparameter %+v: %v - alerts: %+v", pp, err, resp.Alerts)
			}
		}
	}

	//enc := json.NewEncoder(os.Stdout)
	enc.SetIndent("", "  ")
	err = enc.Encode(&alerts)

	return err
}

// enrollServer takes a json file and creates a Server object using the TO API
func enrollServer(toSession *session, r io.Reader) error {
	dec := json.NewDecoder(r)
	var s tc.ServerV5
	err := dec.Decode(&s)
	if err != nil {
		log.Infof("error decoding Server: %v", err)
		return err
	}

	alerts, _, err := toSession.CreateServer(s, client.RequestOptions{})
	if err != nil {
		err = fmt.Errorf("error creating Server: %v - alerts: %+v", err, alerts.Alerts)
		log.Infoln(err)
		return err
	}

	enc := json.NewEncoder(os.Stdout)
	enc.SetIndent("", "  ")
	err = enc.Encode(&alerts)

	return err
}

// enrollServerCapability takes a json file and creates a ServerCapabilityV41 object using the TO API
func enrollServerCapability(toSession *session, r io.Reader) error {
	dec := json.NewDecoder(r)
	var s tc.ServerCapabilityV5
	err := dec.Decode(&s)
	if err != nil {
		err = fmt.Errorf("error decoding Server Capability: %v", err)
		log.Infoln(err)
		return err
	}

	alerts, _, err := toSession.CreateServerCapability(s, client.RequestOptions{})
	if err != nil {
		err = fmt.Errorf("error creating Server Capability: %v - alerts: %+v", err, alerts.Alerts)
		log.Infoln(err)
		return err
	}

	enc := json.NewEncoder(os.Stdout)
	enc.SetIndent("", "  ")
	err = enc.Encode(&alerts)

	return err
}

// enrollFederation takes a json file and creates a Federation object using the TO API.
// It also assigns a Delivery Service, the CDN in a Box admin user, IPv4 resolvers,
// and IPv6 resolvers to that Federation.
func enrollFederation(toSession *session, r io.Reader) error {
	dec := json.NewDecoder(r)
	var federation tc.AllDeliveryServiceFederationsMapping
	err := dec.Decode(&federation)
	if err != nil {
		log.Infof("error decoding Server Capability: %s\n", err)
		return err
	}
	opts := client.NewRequestOptions()
	for _, mapping := range federation.Mappings {
		if mapping.CName == nil || mapping.TTL == nil {
			err = fmt.Errorf("mapping found with null or undefined CName and/or TTL: %+v", mapping)
			log.Errorln(err)
			return err
		}
		var cdnFederation tc.CDNFederationV5
		var cdnName string
		{
			xmlID := string(federation.DeliveryService)
			opts.QueryParameters.Set("xmlId", xmlID)
			deliveryServices, _, err := toSession.GetDeliveryServices(opts)
			opts.QueryParameters.Del("xmlId")
			if err != nil {
				err = fmt.Errorf("getting Delivery Service '%s': %v - alerts: %+v", xmlID, err, deliveryServices.Alerts)
				log.Infoln(err)
				return err
			}
			if len(deliveryServices.Response) != 1 {
				err = fmt.Errorf("wanted 1 Delivery Service with XMLID %s but received %d Delivery Services", xmlID, len(deliveryServices.Response))
				log.Infoln(err)
				return err
			}
			deliveryService := deliveryServices.Response[0]
			cdnName = *deliveryService.CDNName
			cdnFederation = tc.CDNFederationV5{
				CName: *mapping.CName,
				TTL:   *mapping.TTL,
			}
			resp, _, err := toSession.CreateCDNFederation(cdnFederation, cdnName, client.RequestOptions{})
			if err != nil {
				err = fmt.Errorf("creating CDN Federation: %v - alerts: %+v", err, resp.Alerts)
				log.Infoln(err)
				return err
			}
			cdnFederation = resp.Response
			if alerts, _, err := toSession.CreateFederationDeliveryServices(cdnFederation.ID, []int{*deliveryService.ID}, true, client.RequestOptions{}); err != nil {
				err = fmt.Errorf("assigning Delivery Service %s to Federation with ID %d: %v - alerts: %+v", xmlID, cdnFederation.ID, err, alerts.Alerts)
				log.Infoln(err)
				return err
			}
		}
		{
			user, _, err := toSession.GetUserCurrent(client.RequestOptions{})
			if err != nil {
				err = fmt.Errorf("getting the Current User: %v - alerts: %+v", err, user.Alerts)
				log.Infoln(err)
				return err
			}
			if user.Response.ID == nil {
				err = errors.New("current user returned from Traffic Ops had null or undefined ID")
				log.Infoln(err)
				return err
			}
			resp, _, err := toSession.CreateFederationUsers(cdnFederation.ID, []int{*user.Response.ID}, true, client.RequestOptions{})
			if err != nil {
				username := user.Response.Username
				err = fmt.Errorf("assigning User '%s' to Federation with ID %d: %v - alerts: %+v", username, cdnFederation.ID, err, resp.Alerts)
				log.Infoln(err)
				return err
			}
		}
		var allResolverIDs []int
		{
			resolverTypes := []tc.FederationResolverType{tc.FederationResolverType4, tc.FederationResolverType6}
			resolverArrays := [][]string{mapping.Resolve4, mapping.Resolve6}
			for index, resolvers := range resolverArrays {
				resolverIDs, err := createFederationResolversOfType(toSession, resolverTypes[index], resolvers)
				if err != nil {
					return err
				}
				allResolverIDs = append(allResolverIDs, resolverIDs...)
			}
		}
		if resp, _, err := toSession.AssignFederationFederationResolver(cdnFederation.ID, allResolverIDs, true, client.RequestOptions{}); err != nil {
			err = fmt.Errorf("assigning Federation Resolvers to Federation with ID %d: %v - alerts: %+v", cdnFederation.ID, err, resp.Alerts)
			log.Infoln(err)
			return err
		}
		opts.QueryParameters.Set("id", strconv.Itoa(cdnFederation.ID))
		response, _, err := toSession.GetCDNFederations(cdnName, opts)
		opts.QueryParameters.Del("id")
		if err != nil {
			err = fmt.Errorf("getting CDN Federation with ID %d: %v - alerts: %+v", cdnFederation.ID, err, response.Alerts)
			return err
		}
		if len(response.Response) < 1 {
			err = fmt.Errorf("unable to GET a CDN Federation ID %d in CDN %s", cdnFederation.ID, cdnName)
			log.Infoln(err)
			return err
		}
		cdnFederation = response.Response[0]

		enc := json.NewEncoder(os.Stdout)
		enc.SetIndent("", "  ")
		err = enc.Encode(&cdnFederation)
		if err != nil {
			err = fmt.Errorf("encoding CDNFederation %s with ID %d: %v", cdnFederation.CName, cdnFederation.ID, err)
			log.Infoln(err)
			return err
		}
	}
	return err
}

// createFederationResolversOfType creates Federation Resolvers of either RESOLVE4 type or RESOLVE6 type.
func createFederationResolversOfType(toSession *session, resolverTypeName tc.FederationResolverType, ipAddresses []string) ([]int, error) {
	typeNameString := string(resolverTypeName)
	opts := client.NewRequestOptions()
	opts.QueryParameters.Set("name", typeNameString)
	types, _, err := toSession.GetTypes(opts)
	if err != nil {
		err = fmt.Errorf("getting resolver type '%s': %v - alerts: %+v", typeNameString, err, types.Alerts)
		log.Infoln(err)
		return nil, err
	}
	if len(types.Response) < 1 {
		err := fmt.Errorf("unable to get a type with name %s", typeNameString)
		log.Infof(err.Error())
		return nil, err
	}
	typeID := uint(types.Response[0].ID)

	var resolverIDs []int
	for _, ipAddress := range ipAddresses {
		resolver := tc.FederationResolverV5{
			IPAddress: &ipAddress,
			TypeID:    &typeID,
		}
		response, _, err := toSession.CreateFederationResolver(resolver, client.RequestOptions{})
		if err != nil {
			err = fmt.Errorf("creating Federation Resolver with IP address %s: %v - alerts: %+v", ipAddress, err, response.Alerts)
			return nil, err
		}
		if response.Response.ID == nil {

		}
		resolverIDs = append(resolverIDs, int(*response.Response.ID))
	}
	return resolverIDs, nil
}

// enrollServerServerCapability takes a json file and creates a ServerServerCapability object using the TO API
func enrollServerServerCapability(toSession *session, r io.Reader) error {
	dec := json.NewDecoder(r)
	var s tc.ServerServerCapabilityV5
	err := dec.Decode(&s)
	if err != nil {
		err = fmt.Errorf("error decoding Server/Capability relationship: %s", err)
		log.Infoln(err)
		return err
	}
	if s.Server == nil {
		err = errors.New("server/Capability relationship did not specify a server")
		return err
	}

	resp, _, err := toSession.GetServers(client.RequestOptions{QueryParameters: url.Values{"hostName": []string{*s.Server}}})
	if err != nil {
		err = fmt.Errorf("getting server '%s': %v - alerts: %+v", *s.Server, err, resp.Alerts)
		log.Infoln(err)
		return err
	}
	if len(resp.Response) < 1 {
		err = fmt.Errorf("could not find Server %s", *s.Server)
		log.Infoln(err.Error())
		return err
	}
	if len(resp.Response) > 1 {
		err = fmt.Errorf("found more than 1 Server with hostname %s", *s.Server)
		log.Infoln(err.Error())
		return err
	}
	s.ServerID = &resp.Response[0].ID

	alerts, _, err := toSession.CreateServerServerCapability(s, client.RequestOptions{})
	if err != nil {
		err = fmt.Errorf("error creating Server Server Capability: %v - alerts: %+v", err, alerts.Alerts)
		log.Infoln(err.Error())
		return err
	}

	enc := json.NewEncoder(os.Stdout)
	enc.SetIndent("", "  ")
	err = enc.Encode(&alerts)

	return err
}

type dirWatcher struct {
	*fsnotify.Watcher
	TOSession *session
	watched   map[string]func(toSession *session, fn string) error
}

func newDirWatcher(toSession *session) (*dirWatcher, error) {
	var err error
	var dw dirWatcher
	dw.Watcher, err = fsnotify.NewWatcher()
	if err != nil {
		return nil, err
	}
	dw.watched = make(map[string]func(toSession *session, fn string) error)
	go func() {
		const (
			processed = ".processed"
			rejected  = ".rejected"
			retry     = ".retry"
		)
		originalNameRegex := regexp.MustCompile(`(\.retry)*$`)

		emptyCount := map[string]int{}
		const maxEmptyTries = 10

		for {
			select {
			case event, ok := <-dw.Events:
				if !ok {
					log.Infoln("event not ok")
					continue
				}

				// ignore all but Create events
				if event.Op&fsnotify.Create != fsnotify.Create {
					continue
				}

				// skip already processed files
				if strings.HasSuffix(event.Name, processed) || strings.HasSuffix(event.Name, rejected) {
					continue
				}

				i, err := os.Stat(event.Name)
				if err != nil || i.IsDir() {
					log.Infoln("skipping " + event.Name)
					continue
				}
				log.Infoln("new file :", event.Name)

				// what directory is the file in?  Invoke the matching func
				dir := filepath.Base(filepath.Dir(event.Name))
				suffix := rejected
				if f, ok := dw.watched[dir]; ok {
					t := filepath.Base(dir)
					log.Infoln("creating " + t + " from " + event.Name)
					// Sleep for 100 milliseconds so that the file content is probably there when the directory watcher
					// sees the file
					time.Sleep(100 * time.Millisecond)

					err := f(toSession, event.Name)
					// If a file is empty, try reading from it 10 times before giving up on that file
					if err == io.EOF {
						originalName := originalNameRegex.ReplaceAllString(event.Name, "")
						if _, exists := emptyCount[originalName]; !exists {
							emptyCount[originalName] = 0
						}
						emptyCount[originalName]++
						log.Infof("empty json object %s: %s\ntried file %d out of %d times", originalName, err.Error(), emptyCount[originalName], maxEmptyTries)
						if emptyCount[originalName] < maxEmptyTries {
							newName := event.Name + retry
							if err := os.Rename(event.Name, newName); err != nil {
								log.Infof("error renaming %s to %s: %s", event.Name, newName, err)
							}
							continue
						}
					}
					if err != nil {
						log.Infof("error creating %s from %s: %s\n", dir, event.Name, err.Error())
					} else {
						suffix = processed
					}
				} else {
					log.Infof("no method for creating %s\n", dir)
				}
				// rename the file indicating if processed or rejected
				err = os.Rename(event.Name, event.Name+suffix)
				if err != nil {
					log.Infof("error renaming %s to %s: %s\n", event.Name, event.Name+suffix, err.Error())
				}
			case err, ok := <-dw.Errors:
				log.Infof("error from fsnotify: ok? %v;  error: %v\n", ok, err)
				continue
			}
		}
	}()
	return &dw, err
}

// watch starts f when a new file is created in dir
func (dw *dirWatcher) watch(watchdir, t string, f func(*session, io.Reader) error) {
	dir := watchdir + "/" + t
	if stat, err := os.Stat(dir); err != nil || !stat.IsDir() {
		// attempt to create dir
		if err = os.Mkdir(dir, os.ModeDir|0700); err != nil {
			log.Infoln("cannot watch " + dir + ": not a directory")
			return
		}
	}

	log.Infoln("watching " + dir)
	dw.Add(dir)
	dw.watched[t] = func(toSession *session, fn string) error {
		fh, err := os.Open(fn)
		if err != nil {
			return err
		}
		defer log.Close(fh, "could not close file")
		return f(toSession, fh)
	}
}

func startWatching(watchDir string, toSession *session, dispatcher map[string]func(*session, io.Reader) error) (*dirWatcher, error) {
	// watch for file creation in directories
	dw, err := newDirWatcher(toSession)
	if err == nil {
		for d, f := range dispatcher {
			dw.watch(watchDir, d, f)
		}
	}
	return dw, err
}

func startServer(httpPort string, toSession *session, dispatcher map[string]func(*session, io.Reader) error) error {
	baseEP := "/api/4.0/"
	for d, f := range dispatcher {
		http.HandleFunc(baseEP+d, func(w http.ResponseWriter, r *http.Request) {
			defer log.Close(r.Body, "could not close reader")
			f(toSession, r.Body)
		})
	}

	go func() {
		server := &http.Server{
			Addr:      httpPort,
			TLSConfig: nil,
			ErrorLog:  log.Error,
		}
		if err := server.ListenAndServe(); err != nil {
			log.Errorf("stopping server: %v\n", err)
			panic(err)
		}
	}()

	log.Infoln("http service started on " + httpPort)
	return nil
}

// Set up the log config -- all messages go to stdout
type logConfig struct{}

func (cfg logConfig) ErrorLog() log.LogLocation {
	return log.LogLocationStdout
}
func (cfg logConfig) WarningLog() log.LogLocation {
	return log.LogLocationStdout
}
func (cfg logConfig) InfoLog() log.LogLocation {
	return log.LogLocationStdout
}
func (cfg logConfig) DebugLog() log.LogLocation {
	return log.LogLocationStdout
}
func (cfg logConfig) EventLog() log.LogLocation {
	return log.LogLocationStdout
}

func main() {
	var watchDir, httpPort string

	flag.StringVar(&startedFile, "started", startedFile, "file indicating service was started")
	flag.StringVar(&watchDir, "dir", "", "base directory to watch")
	flag.StringVar(&httpPort, "http", "", "act as http server for POST on this port (e.g. :7070)")
	flag.Parse()

	err := log.InitCfg(logConfig{})
	if err != nil {
		panic(err.Error())
	}
	if watchDir == "" && httpPort == "" {
		// if neither -dir nor -http provided, default to watching the current dir
		watchDir = "."
	}

	var toCreds struct {
		URL      string `envconfig:"TO_URL"`
		User     string `envconfig:"TO_USER"`
		Password string `envconfig:"TO_PASSWORD"`
	}

	envconfig.Process("", &toCreds)

	reqTimeout := time.Second * time.Duration(60)

	log.Infoln("Starting TrafficOps session")
	toSession, err := newSession(reqTimeout, toCreds.URL, toCreds.User, toCreds.Password)
	if err != nil {
		log.Errorln("error starting TrafficOps session: " + err.Error())
		os.Exit(1)
	}
	log.Infoln("TrafficOps session established")

	// dispatcher maps an API endpoint name to a function to act on the JSON input Reader
	dispatcher := map[string]func(*session, io.Reader) error{
		"types":                                  enrollType,
		"cdns":                                   enrollCDN,
		"cachegroups":                            enrollCachegroup,
		"topologies":                             enrollTopology,
		"profiles":                               enrollProfile,
		"parameters":                             enrollParameter,
		"servers":                                enrollServer,
		"server_capabilities":                    enrollServerCapability,
		"server_server_capabilities":             enrollServerServerCapability,
		"asns":                                   enrollASN,
		"deliveryservices":                       enrollDeliveryService,
		"deliveryservices_required_capabilities": enrollDeliveryServicesRequiredCapability,
		"deliveryservice_servers":                enrollDeliveryServiceServer,
		"divisions":                              enrollDivision,
		"federations":                            enrollFederation,
		"origins":                                enrollOrigin,
		"phys_locations":                         enrollPhysLocation,
		"regions":                                enrollRegion,
		"statuses":                               enrollStatus,
		"tenants":                                enrollTenant,
		"users":                                  enrollUser,
	}

	if len(httpPort) != 0 {
		log.Infoln("Starting http server on " + httpPort)
		err := startServer(httpPort, &toSession, dispatcher)
		if err != nil {
			log.Errorln("http server on " + httpPort + " failed: " + err.Error())
		}
	}

	if len(watchDir) != 0 {
		log.Infoln("Watching directory " + watchDir)
		dw, err := startWatching(watchDir, &toSession, dispatcher)
		defer log.Close(dw, "could not close dirwatcher")
		if err != nil {
			log.Errorf("dirwatcher on %s failed: %s", watchDir, err.Error())
		}
	}

	// create this file to indicate the enroller is ready
	f, err := os.Create(startedFile)
	if err != nil {
		panic(err)
	}
	log.Infoln("Created " + startedFile)
	log.Close(f, "could not close file")

	var waitforever chan struct{}
	<-waitforever
}
