package main

import (
	"fmt"
	"log"
	"net"
	"os"
	"path"
	"strings"
	"time"

	"github.com/miekg/dns"
)

var reqTypes map[uint16]string

var weekdays []string

type logItem struct {
	clientIP  string
	name      string
	reqType   uint16
	config    string
	timetable string
	killed    string
}

// logChannel used by logging thread
var logChannel chan logItem

func init() {

	weekdays = []string{"Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"}

	if len(ZabovDebugDBPath) > 0 {
		os.MkdirAll(ZabovDebugDBPath, 0755)
	}

	reqTypes = map[uint16]string{
		dns.TypeNone:       "TypeNone",
		dns.TypeA:          "TypeA",
		dns.TypeNS:         "TypeNS",
		dns.TypeMD:         "TypeMD",
		dns.TypeMF:         "TypeMF",
		dns.TypeCNAME:      "TypeCNAME",
		dns.TypeSOA:        "TypeSOA",
		dns.TypeMB:         "TypeMB",
		dns.TypeMG:         "TypeMG",
		dns.TypeMR:         "TypeMR",
		dns.TypeNULL:       "TypeNULL",
		dns.TypePTR:        "TypePTR",
		dns.TypeHINFO:      "TypeHINFO",
		dns.TypeMINFO:      "TypeMINFO",
		dns.TypeMX:         "TypeMX",
		dns.TypeTXT:        "TypeTXT",
		dns.TypeRP:         "TypeRP",
		dns.TypeAFSDB:      "TypeAFSDB",
		dns.TypeX25:        "TypeX25",
		dns.TypeISDN:       "TypeISDN",
		dns.TypeRT:         "TypeRT",
		dns.TypeNSAPPTR:    "TypeNSAPPTR",
		dns.TypeSIG:        "TypeSIG",
		dns.TypeKEY:        "TypeKEY",
		dns.TypePX:         "TypePX",
		dns.TypeGPOS:       "TypeGPOS",
		dns.TypeAAAA:       "TypeAAAA",
		dns.TypeLOC:        "TypeLOC",
		dns.TypeNXT:        "TypeNXT",
		dns.TypeEID:        "TypeEID",
		dns.TypeNIMLOC:     "TypeNIMLOC",
		dns.TypeSRV:        "TypeSRV",
		dns.TypeATMA:       "TypeATMA",
		dns.TypeNAPTR:      "TypeNAPTR",
		dns.TypeKX:         "TypeKX",
		dns.TypeCERT:       "TypeCERT",
		dns.TypeDNAME:      "TypeDNAME",
		dns.TypeOPT:        "TypeOPT",
		dns.TypeAPL:        "TypeAPL",
		dns.TypeDS:         "TypeDS",
		dns.TypeSSHFP:      "TypeSSHFP",
		dns.TypeRRSIG:      "TypeRRSIG",
		dns.TypeNSEC:       "TypeNSEC",
		dns.TypeDNSKEY:     "TypeDNSKEY",
		dns.TypeDHCID:      "TypeDHCID",
		dns.TypeNSEC3:      "TypeNSEC3",
		dns.TypeNSEC3PARAM: "TypeNSEC3PARAM",
		dns.TypeTLSA:       "TypeTLSA",
		dns.TypeSMIMEA:     "TypeSMIMEA",
		dns.TypeHIP:        "TypeHIP",
		dns.TypeNINFO:      "TypeNINFO",
		dns.TypeRKEY:       "TypeRKEY",
		dns.TypeTALINK:     "TypeTALINK",
		dns.TypeCDS:        "TypeCDS",
		dns.TypeCDNSKEY:    "TypeCDNSKEY",
		dns.TypeOPENPGPKEY: "TypeOPENPGPKEY",
		dns.TypeCSYNC:      "TypeCSYNC",
		dns.TypeSPF:        "TypeSPF",
		dns.TypeUINFO:      "TypeUINFO",
		dns.TypeUID:        "TypeUID",
		dns.TypeGID:        "TypeGID",
		dns.TypeUNSPEC:     "TypeUNSPEC",
		dns.TypeNID:        "TypeNID",
		dns.TypeL32:        "TypeL32",
		dns.TypeL64:        "TypeL64",
		dns.TypeLP:         "TypeLP",
		dns.TypeEUI48:      "TypeEUI48",
		dns.TypeEUI64:      "TypeEUI64",
		dns.TypeURI:        "TypeURI",
		dns.TypeCAA:        "TypeCAA",
		dns.TypeAVC:        "TypeAVC",
		dns.TypeTKEY:       "TypeTKEY",
		dns.TypeTSIG:       "TypeTSIG",
		dns.TypeIXFR:       "TypeIXFR",
		dns.TypeAXFR:       "TypeAXFR",
		dns.TypeMAILB:      "TypeMAILB",
		dns.TypeMAILA:      "TypeMAILA",
		dns.TypeANY:        "TypeANY",
		dns.TypeTA:         "TypeTA",
		dns.TypeDLV:        "TypeDLV",
		dns.TypeReserved:   "TypeReserved"}

	fmt.Println("Local Time:", getLocalTime().Format(time.ANSIC))

	if len(ZabovDebugDBPath) > 0 {
		logChannel = make(chan logItem, 1024)
		go logWriteThread()
	}
}

func logWriteThread() {
	for item := range logChannel {
		var header string
		d := time.Now().Format("2006-01-02")
		logpath := path.Join(ZabovDebugDBPath, strings.Replace(item.clientIP, ":", "_", -1)+"-"+d+".log")

		_, err1 := os.Stat(logpath)
		if os.IsNotExist(err1) {
			header = strings.Join([]string{"time", "clientIP", "name", "reqType", "config", "timetable", "killed"}, "\t")
		}
		f, err := os.OpenFile(logpath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
		if err == nil {
			reqTypeName, err := reqTypes[item.reqType]
			if !err {
				reqTypeName = fmt.Sprintf("%d", item.reqType)
			}
			ct := time.Now().Format(time.RFC3339)
			log := strings.Join([]string{ct, item.clientIP, strings.TrimRight(item.name, "."), reqTypeName, item.config, item.timetable, item.killed}, "\t")
			if len(header) > 0 {
				f.Write([]byte(header))
				f.Write([]byte("\n"))
			}
			f.Write([]byte(log))
			f.Write([]byte("\n"))
			f.Close()
		}
	}
}

func logQuery(clientIP string, name string, reqType uint16, config string, timetable string, killed string) {
	if len(ZabovDebugDBPath) > 0 {
		k := logItem{clientIP: clientIP, name: name, reqType: reqType, config: config, timetable: timetable, killed: killed}

		logChannel <- k

	}
}

func getLocalTime() time.Time {
	return time.Now().Local()
}

func confFromTimeTable(timetable string) string {
	tt := ZabovTimetables[timetable]
	if tt == nil {
		if ZabovDebug {
			log.Println("confFromTimeTable: return default")
		}
		return "default"
	}
	for _, ttentry := range tt.table {
		now := getLocalTime()

		nowHour := now.Hour()
		nowMinute := now.Minute()
		weekday := weekdays[now.Weekday()]
		if ttentry.days == nil || len(ttentry.days) == 0 || ttentry.days[weekday] || ttentry.days[strings.ToLower(weekday)] {
			for _, t := range ttentry.times {

				if (nowHour > t.start.hour || (nowHour == t.start.hour && nowMinute >= t.start.minute)) &&
					(nowHour < t.stop.hour || (nowHour == t.stop.hour && nowMinute <= t.stop.minute)) {
					go incrementStats("TIMETABLE IN: "+timetable, 1)
					if ZabovDebug {
						log.Println("confFromTimeTable: return IN", tt.cfgin)
					}
					return tt.cfgin
				}
			}
		}
	}
	go incrementStats("TIMETABLE OUT: "+timetable, 1)
	if ZabovDebug {
		log.Println("confFromTimeTable: return OUT", tt.cfgout)
	}
	return tt.cfgout
}

func confFromIP(clientIP net.IP) (string, string) {
	for _, ipgroup := range ZabovIPGroups {
		for _, ip := range ipgroup.ips {
			if clientIP.Equal(ip) {
				if len(ipgroup.timetable) > 0 {
					return confFromTimeTable(ipgroup.timetable), ipgroup.timetable
				}
				if ZabovDebug {
					log.Println("confFromIP: ipgroup.cfg", ipgroup.cfg)
				}
				return ipgroup.cfg, ""
			}
		}
	}
	if len(ZabovDefaultTimetable) > 0 {
		return confFromTimeTable(ZabovDefaultTimetable), ZabovDefaultTimetable
	}

	if ZabovDebug {
		log.Println("confFromIP: return default")
	}
	return "default", ""
}
func (mydns *handler) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
	go incrementStats("TotalQueries", 1)

	remIP, _, e := net.SplitHostPort(w.RemoteAddr().String())
	if e != nil {
		go incrementStats("CLIENT ERROR: "+remIP, 1)
	} else {
		go incrementStats("CLIENT: "+remIP, 1)
	}

	msg := dns.Msg{}
	msg.SetReply(r)

	config, timetable := confFromIP(net.ParseIP(remIP))

	if ZabovDebug {
		log.Println("REQUEST:", remIP, config)
	}
	ZabovConfig := ZabovConfigs[config]
	QType := r.Question[0].Qtype
	switch QType {
	case dns.TypeA:
		msg.Authoritative = true
		domain := msg.Question[0].Name
		fqdn := strings.TrimRight(domain, ".")

		if ZabovDebug {
			log.Println("TypeA: fqdn:", fqdn)
		}

		if len(ZabovIPAliases[fqdn]) > 0 {
			config = "__aliases__"
			msg.Answer = append(msg.Answer, &dns.A{
				Hdr: dns.RR_Header{Name: domain, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 60},
				A:   net.ParseIP(ZabovIPAliases[fqdn]),
			})
			go logQuery(remIP, fqdn, QType, config, timetable, "alias")
			break
		}
		if len(ZabovLocalResponder) > 0 {
			if !strings.Contains(fqdn, ".") ||
				(len(ZabovLocalDomain) > 0 && strings.HasSuffix(fqdn, ZabovLocalDomain)) {
				config = localresponderConfigName
				ret := ForwardQuery(r, config, true)
				w.WriteMsg(ret)
				go logQuery(remIP, fqdn, QType, config, timetable, "localresponder")
				break
			}

		}
		if !domainInWhiteListfile(fqdn, config) && domainInKillfile(fqdn, config) {
			go incrementStats("Killed", 1)

			msg.Answer = append(msg.Answer, &dns.A{
				Hdr: dns.RR_Header{Name: domain, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 60},
				A:   ZabovConfig.ZabovAddBL,
			})
			go logQuery(remIP, fqdn, QType, config, timetable, "killed")
		} else {
			go logQuery(remIP, fqdn, QType, config, timetable, "forwarded")
			ret := ForwardQuery(r, config, !ZabovConfig.ZabovCache)
			w.WriteMsg(ret)
		}
	case dns.TypePTR:
		if ZabovDebug {
			log.Println("TypePTR: Name:", msg.Question[0].Name)
		}

		if len(ZabovLocalResponder) > 0 {
			// if set use local responder for reverse lookup (suffix ".in-addr.arpa.")
			config = localresponderConfigName
		}
		ret := ForwardQuery(r, config, true)
		w.WriteMsg(ret)
		go logQuery(remIP, msg.Question[0].Name, QType, config, timetable, "localresponder")
	default:
		ret := ForwardQuery(r, config, !ZabovConfig.ZabovCache)
		w.WriteMsg(ret)
		if len(ZabovDebugDBPath) > 0 {
			go logQuery(remIP, msg.Question[0].Name, QType, config, timetable, "forwarded")
		}
	}
	go incrementStats("CONFIG: "+config, 1)
	w.WriteMsg(&msg)

}
