package stats

import (
	"bufio"
	"bytes"
	"context"
	"fmt"
	"io/ioutil"
	"net"
	"os"
	"path/filepath"
	"strconv"
	"strings"
)

var (
	delim = []byte{' '}
)

// Host represents host status.
type Host struct {
	Sysname    string
	Storages   []*Storage
	Interfaces []*Interface
}

// MemStats represents the memory statistics.
type MemStats struct {
	Total       int64 // total memory in byte
	PageSize    int64 // a page size in byte
	KernelPages int64
	UserPages   Gauge
	SwapPages   Gauge

	Malloced Gauge // kernel malloced data in byte
	Graphics Gauge // kernel graphics data in byte
}

// Gauge is used/available gauge.
type Gauge struct {
	Used  int64
	Avail int64
}

func (g Gauge) Free() int64 {
	return g.Avail - g.Used
}

// ReadMemStats reads memory statistics from /dev/swap.
func ReadMemStats(ctx context.Context, opts ...Option) (*MemStats, error) {
	cfg := newConfig(opts...)
	swap := filepath.Join(cfg.rootdir, "/dev/swap")
	f, err := os.Open(swap)
	if err != nil {
		return nil, err
	}
	defer f.Close()

	var stat MemStats
	m := map[string]interface{}{
		"memory":        &stat.Total,
		"pagesize":      &stat.PageSize,
		"kernel":        &stat.KernelPages,
		"user":          &stat.UserPages,
		"swap":          &stat.SwapPages,
		"kernel malloc": &stat.Malloced,
		"kernel draw":   &stat.Graphics,
	}
	scanner := bufio.NewScanner(f)
	for scanner.Scan() {
		fields := bytes.SplitN(scanner.Bytes(), delim, 2)
		if len(fields) < 2 {
			continue
		}
		switch key := string(fields[1]); key {
		case "memory", "pagesize", "kernel":
			v := m[key].(*int64)
			n, err := strconv.ParseInt(string(fields[0]), 10, 64)
			if err != nil {
				return nil, err
			}
			*v = n
		case "user", "swap", "kernel malloc", "kernel draw":
			v := m[key].(*Gauge)
			if err := parseGauge(string(fields[0]), v); err != nil {
				return nil, err
			}
		}
	}
	if err := scanner.Err(); err != nil {
		return nil, err
	}
	return &stat, nil
}

func parseGauge(s string, r *Gauge) error {
	a := strings.SplitN(s, "/", 2)
	if len(a) != 2 {
		return fmt.Errorf("can't parse ratio: %s", s)
	}
	var p intParser
	u := p.ParseInt64(a[0], 10)
	n := p.ParseInt64(a[1], 10)
	if err := p.Err(); err != nil {
		return err
	}
	r.Used = u
	r.Avail = n
	return nil
}

type Storage struct {
	Name     string
	Model    string
	Capacity int64
}

type Interface struct {
	Name string
	Addr string
}

const (
	numEther = 8  // see ether(3)
	numIpifc = 16 // see ip(3)
)

// ReadInterfaces reads network interfaces from etherN.
func ReadInterfaces(ctx context.Context, opts ...Option) ([]*Interface, error) {
	cfg := newConfig(opts...)
	var a []*Interface
	for i := 0; i < numEther; i++ {
		p, err := readInterface(cfg.rootdir, i)
		if os.IsNotExist(err) {
			continue
		}
		if err != nil {
			return nil, err
		}
		a = append(a, p)
	}
	return a, nil
}

func readInterface(netroot string, i int) (*Interface, error) {
	ether := fmt.Sprintf("ether%d", i)
	dir := filepath.Join(netroot, ether)
	info, err := os.Stat(dir)
	if err != nil {
		return nil, err
	}
	if !info.IsDir() {
		return nil, fmt.Errorf("%s: is not directory", dir)
	}

	addr, err := ioutil.ReadFile(filepath.Join(dir, "addr"))
	if err != nil {
		return nil, err
	}
	return &Interface{
		Name: ether,
		Addr: string(addr),
	}, nil
}

var (
	netdirs = []string{"/net", "/net.alt"}
)

// ReadHost reads host status.
func ReadHost(ctx context.Context, opts ...Option) (*Host, error) {
	cfg := newConfig(opts...)
	var h Host
	name, err := readSysname(cfg.rootdir)
	if err != nil {
		return nil, err
	}
	h.Sysname = name

	a, err := readStorages(cfg.rootdir)
	if err != nil {
		return nil, err
	}
	h.Storages = a

	for _, s := range netdirs {
		netroot := filepath.Join(cfg.rootdir, s)
		ifaces, err := ReadInterfaces(ctx, WithRootDir(netroot))
		if err != nil {
			return nil, err
		}
		h.Interfaces = append(h.Interfaces, ifaces...)
	}
	return &h, nil
}

func readSysname(rootdir string) (string, error) {
	file := filepath.Join(rootdir, "/dev/sysname")
	b, err := ioutil.ReadFile(file)
	if err != nil {
		return "", err
	}
	return string(bytes.TrimSpace(b)), nil
}

func readStorages(rootdir string) ([]*Storage, error) {
	sdctl := filepath.Join(rootdir, "/dev/sdctl")
	f, err := os.Open(sdctl)
	if err != nil {
		return nil, err
	}
	defer f.Close()

	var a []*Storage
	scanner := bufio.NewScanner(f)
	for scanner.Scan() {
		fields := bytes.Split(scanner.Bytes(), delim)
		if len(fields) == 0 {
			continue
		}
		exp := string(fields[0]) + "*"
		if !strings.HasPrefix(exp, "sd") {
			continue
		}
		dir := filepath.Join(rootdir, "/dev", exp)
		m, err := filepath.Glob(dir)
		if err != nil {
			return nil, err
		}
		for _, dir := range m {
			s, err := readStorage(dir)
			if err != nil {
				return nil, err
			}
			a = append(a, s)
		}
	}
	if err := scanner.Err(); err != nil {
		return nil, err
	}
	return a, nil
}

func readStorage(dir string) (*Storage, error) {
	ctl := filepath.Join(dir, "ctl")
	f, err := os.Open(ctl)
	if err != nil {
		return nil, err
	}
	defer f.Close()

	var s Storage
	s.Name = filepath.Base(dir)
	scanner := bufio.NewScanner(f)
	for scanner.Scan() {
		line := scanner.Bytes()
		switch {
		case bytes.HasPrefix(line, []byte("inquiry")):
			s.Model = string(bytes.TrimSpace(line[7:]))
		case bytes.HasPrefix(line, []byte("geometry")):
			fields := bytes.Split(line, delim)
			if len(fields) < 3 {
				continue
			}
			var p intParser
			sec := p.ParseInt64(string(fields[1]), 10)
			size := p.ParseInt64(string(fields[2]), 10)
			if err := p.Err(); err != nil {
				return nil, err
			}
			s.Capacity = sec * size
		}
	}
	if err := scanner.Err(); err != nil {
		return nil, err
	}
	return &s, nil
}

type IPStats struct {
	ID      int    // number of interface in ipifc dir
	Device  string // associated physical device
	MTU     int    // max transfer unit
	Sendra6 uint8  // on == send router adv
	Recvra6 uint8  // on == recv router adv

	Pktin  int64 // packets read
	Pktout int64 // packets written
	Errin  int64 // read errors
	Errout int64 // write errors
}

type Iplifc struct {
	IP            net.IP
	Mask          net.IPMask
	Net           net.IP // ip & mask
	PerfLifetime  int64  // preferred lifetime
	ValidLifetime int64  // valid lifetime
}

type Ipv6rp struct {
	// TODO(lufia): see ip(2)
}
