package service

import (
	"crypto/tls"
	"errors"
	log "github.com/cihub/seelog"
	"github.com/google/gopacket"
	"github.com/google/gopacket/layers"
	//"git.loafle.net/overflow/overflow_probe/central/client/events"
	//"git.loafle.net/overflow/overflow_probe/discovery/communicate"
	"git.loafle.net/overflow/overflow_probe/discovery/core/scan/service/matcher"
	"git.loafle.net/overflow/overflow_probe/discovery/core/scan/service/matcher/packet"
	"git.loafle.net/overflow/overflow_probe/discovery/discovery/types"
	"net"
	"strconv"
	"strings"
)

func Scan(dp *types.DiscoveryPort) {
	ip := dp.Host.Ip_
	port := dp.Port_

	factories := []ConnFactory{
		ConnFactoryNormal{t: types.TYPE_TCP},
		ConnFactoryTLS{t: types.TYPE_TLS},
	}
	info := types.NewServiceScanInfo(dp)

	for i := 0; i < len(factories); i++ {
		m := factories[i]

		isFound := false
		conn, err := m.createSocket(ip, port)
		if err != nil {
			log.Error("An error has occurred on " + m.Type() + " " + ip + ":" + port + " - " + err.Error())
			break
		}

		buf := make([]byte, 1024)
		l, err := conn.Read(buf)
		if err != nil {
			l = 0
		}
		if l != 0 {
			isFound = processPrePacket(conn, packet.NewPacket(buf, l), matcher.GetTcpMatchers(true), info, m)
		} else {
			conn.Close()
			isFound = processPostPacket(matcher.GetTcpMatchers(false), info, m)
		}
		if isFound == true {
			break
		}
	}
}

func Check(dp *types.DiscoveryPort, services []*types.DiscoveryService) error {

	if len(services) <= 0 {
		return errors.New("No service has bound to this port.")
	}
	for _, service := range services {

		m := matcher.GetMatcherByName(service.ServiceName)
		if m == nil {
			return errors.New("Not supported service : " + service.ServiceName)
		}

		addr := dp.Host.Ip_ + ":" + dp.Port_
		return checkService(m, addr, dp.PortType)
	}

	return nil
}

func checkService(m matcher.Matcher, addr, portType string) error {
	conn, err := getConnection(portType, addr)
	if err != nil {
		return err
	}
	defer conn.Close()

	for i := 0; i < m.PacketCount(); i++ {
		pack := m.Packet(i)
		conn.Write(pack.Buffer)
		bytes := make([]byte, 1024)
		n, _ := conn.Read(bytes)

		p := packet.NewPacket(bytes, n)

		if m.Match(i, p, nil) {
			return nil
		}
	}

	return errors.New("Fail")
}

func getConnection(portType, addr string) (net.Conn, error) {
	var conn net.Conn
	var err error
	if portType == types.TYPE_TLS {
		conn, err = tls.Dial(
			"tcp",
			addr,
			&tls.Config{
				InsecureSkipVerify: true,
				ServerName:         addr,
			},
		)
	} else {
		conn, err = net.Dial(strings.ToLower(portType), addr)
	}

	if err != nil {
		return nil, err
	}

	return conn, nil

}

func MatchUDPPacket(zone *types.DiscoveryZone, ipString, portString string, udpLayer gopacket.Layer) {

	host, _ := zone.GetHost(ipString)
	port := types.NewPort(portString, host, types.TYPE_UDP)

	mats := matcher.GetUdpMatchers()
	info := types.NewServiceScanInfo(port)

	for i := 0; i < len(mats); i++ {
		matcher := mats[i]
		p := packet.NewPacket(udpLayer.LayerPayload(), len(udpLayer.LayerPayload()))

		if matcher.Match(0, p, info) {
			key := portString + ":" + types.TYPE_UDP
			host.SetPort(key, port)

			ser := types.NewService(types.TYPE_UDP, matcher.ServiceName(), info.Port)
			AddService(ser, info)
		}
	}
}

func AddService(ds *types.DiscoveryService, info *types.ServiceScanInfo) {
	port := info.Port
	var serviceName string
	if ds != nil {
		port.Services = append(port.Services, ds)
		port.SetHistory(info.History)
		serviceName = ds.ServiceName
		log.Debugf("%s, %s:%s, Service was found : %s  ", ds.PortType, port.Host.Ip_, port.Port_, ds.ServiceName)
	} else {
		i, _ := strconv.Atoi(port.Port_)
		p := uint16(i)
		expectName := layers.TCPPortNames[layers.TCPPort(p)]
		serviceName = "Unknown Service was found. (Perhaps : " + expectName + ")"
		log.Debugf("%s:%s. %s", port.Host.Ip_, port.Port_, serviceName)
	}

	sendEvent(info, serviceName)
}

func AddServiceScanHistory(histories *[]*types.ServiceScanHistory, h *types.ServiceScanHistory) {
	*histories = append(*histories, h)
}

func sendEvent(info *types.ServiceScanInfo, serviceName string) {

	//communicate.Send(
	//	events.NewEvent(
	//		events.CENTRAL_EVENT,
	//		events.NewServiceEndEvent(
	//			info.Port.Host.Zone.CidrInt64(),
	//			info.Port.Host.Ip,
	//			info.Port.Number,
	//			serviceName)))
}
