package device

import (
	"crypto/rand"
	"encoding/hex"
	"io"
	"log"
	"net"
	"net/http"
	"strings"

	"git.autistici.org/id/auth"
	"github.com/gorilla/sessions"
	"github.com/mssola/user_agent"
)

func randomDeviceID() string {
	b := make([]byte, 8)
	if _, err := io.ReadFull(rand.Reader, b[:]); err != nil {
		panic(err)
	}
	return hex.EncodeToString(b)
}

// Manager can provide DeviceInfo entries for incoming HTTP requests.
type Manager struct {
	store sessions.Store
	geodb *geoIPDb
}

// Config stores options for the device info manager.
type Config struct {
	AuthKey        string   `yaml:"auth_key"`
	GeoIPDataFiles []string `yaml:"geo_ip_data_files"`
}

// New returns a new Manager with the given configuration.
func New(config *Config) (*Manager, error) {
	if config == nil {
		config = &Config{}
	}

	geodb, err := newGeoIP(config.GeoIPDataFiles)
	if err != nil {
		log.Printf("Warning: GeoIP disabled: %v", err)
	}

	return &Manager{
		geodb: geodb,
		store: newStore([]byte(config.AuthKey)),
	}, nil
}

const deviceIDSessionName = "_dev"

// GetDeviceInfoFromRequest will retrieve or create a DeviceInfo
// object for the given request. It will always return a valid object.
// The ResponseWriter is needed to store the unique ID on the client
// when a new device info object is created.
func (m *Manager) GetDeviceInfoFromRequest(w http.ResponseWriter, req *http.Request) *auth.DeviceInfo {
	session, _ := m.store.Get(req, deviceIDSessionName)
	devID, ok := session.Values["id"].(string)
	if !ok || devID == "" {
		// Generate a new Device ID and save it on the client.
		devID = randomDeviceID()
		session.Values["id"] = devID
		if err := session.Save(req, w); err != nil {
			// This is likely a misconfiguration issue, so
			// we want to know about it.
			log.Printf("error saving device manager session: %v", err)
		}
	}

	uaStr := req.UserAgent()
	ua := user_agent.New(uaStr)
	browser, _ := ua.Browser()
	d := auth.DeviceInfo{
		ID:        devID,
		UserAgent: uaStr,
		Mobile:    ua.Mobile(),
		OS:        ua.OS(),
		Browser:   browser,
	}

	// Special check for .onion HTTP hosts - sets the zone to
	// "onion" and skips the IP-based checks.
	if req.Host != "" && strings.HasSuffix(req.Host, ".onion") {
		d.RemoteZone = "onion"
	} else {
		// If we have an IP address, we can add more information.
		if ip := getIPFromRequest(req); ip != nil {
			d.RemoteAddr = ip.String()

			// Country lookup.
			if m.geodb != nil {
				if zone, err := m.geodb.getZoneForIP(ip); err == nil {
					d.RemoteZone = zone
				}
			}
		}
	}

	return &d
}

func getIPFromRequest(req *http.Request) net.IP {
	// Parse the RemoteAddr Request field, for starters.
	host, _, err := net.SplitHostPort(req.RemoteAddr)
	if err != nil {
		host = req.RemoteAddr
	}
	return net.ParseIP(host)
}