package onvif

import (
	"bytes"
	"encoding/json"
	"encoding/xml"
	"errors"
	"fmt"
	"io/ioutil"
	"net/http"
	"net/url"
	"reflect"
	"strconv"
	"strings"

	"github.com/kerberos-io/onvif/networking"
	"github.com/kerberos-io/onvif/xsd/onvif"

	"github.com/beevik/etree"
	"github.com/kerberos-io/onvif/device"
	"github.com/kerberos-io/onvif/gosoap"
)

// Xlmns XML Scheam
var Xlmns = map[string]string{
	"onvif":   "http://www.onvif.org/ver10/schema",
	"tds":     "http://www.onvif.org/ver10/device/wsdl",
	"trt":     "http://www.onvif.org/ver10/media/wsdl",
	"tr2":     "http://www.onvif.org/ver20/media/wsdl",
	"tev":     "http://www.onvif.org/ver10/events/wsdl",
	"tptz":    "http://www.onvif.org/ver20/ptz/wsdl",
	"timg":    "http://www.onvif.org/ver20/imaging/wsdl",
	"tan":     "http://www.onvif.org/ver20/analytics/wsdl",
	"xmime":   "http://www.w3.org/2005/05/xmlmime",
	"wsnt":    "http://docs.oasis-open.org/wsn/b-2",
	"xop":     "http://www.w3.org/2004/08/xop/include",
	"wsa":     "http://www.w3.org/2005/08/addressing",
	"wstop":   "http://docs.oasis-open.org/wsn/t-1",
	"wsntw":   "http://docs.oasis-open.org/wsn/bw-2",
	"wsrf-rw": "http://docs.oasis-open.org/wsrf/rw-2",
	"wsaw":    "http://www.w3.org/2006/05/addressing/wsdl",
	"tt":      "http://www.onvif.org/ver10/recording/wsdl",
	"wsse":    "http://docs.oasis-open.org/wss/2004/01/oasis-200401",
	"wsu":     "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd",
}

// DeviceType alias for int
type DeviceType int

// Onvif Device Tyoe
const (
	NVD DeviceType = iota
	NVS
	NVA
	NVT

	ContentType = "Content-Type"
)

func (devType DeviceType) String() string {
	stringRepresentation := []string{
		"NetworkVideoDisplay",
		"NetworkVideoStorage",
		"NetworkVideoAnalytics",
		"NetworkVideoTransmitter",
	}
	i := uint8(devType)
	switch {
	case i <= uint8(NVT):
		return stringRepresentation[i]
	default:
		return strconv.Itoa(int(i))
	}
}

// DeviceInfo struct contains general information about ONVIF device
type DeviceInfo struct {
	Name            string
	Manufacturer    string
	Model           string
	FirmwareVersion string
	SerialNumber    string
	HardwareId      string
}

// Device for a new device of onvif and DeviceInfo
// struct represents an abstract ONVIF device.
// It contains methods, which helps to communicate with ONVIF device
type Device struct {
	params       DeviceParams
	endpoints    map[string]string
	info         DeviceInfo
	digestClient *DigestClient
}

type DeviceParams struct {
	Xaddr              string
	EndpointRefAddress string
	Username           string
	Password           string
	HttpClient         *http.Client
	AuthMode           string
}

// GetServices return available endpoints
func (dev *Device) GetServices() map[string]string {
	return dev.endpoints
}

// GetServices return available endpoints
func (dev *Device) GetDeviceInfo() DeviceInfo {
	return dev.info
}

// SetDeviceInfoFromScopes goes through the scopes and sets the device info fields for supported categories (currently name and hardware).
// See 7.3.2.2 Scopes in the ONVIF Core Specification (https://www.onvif.org/specs/core/ONVIF-Core-Specification.pdf).
func (dev *Device) SetDeviceInfoFromScopes(scopes []string) {
	newInfo := dev.info
	supportedScopes := []struct {
		category string
		setField func(s string)
	}{
		{category: "name", setField: func(s string) { newInfo.Name = s }},
		{category: "hardware", setField: func(s string) { newInfo.Model = s }},
	}

	for _, s := range scopes {
		for _, supp := range supportedScopes {
			fullScope := fmt.Sprintf("onvif://www.onvif.org/%s/", supp.category)
			scopeValue, matchesScope := strings.CutPrefix(s, fullScope)
			if matchesScope {
				unescaped, err := url.QueryUnescape(scopeValue)
				if err != nil {
					continue
				}
				supp.setField(unescaped)
			}
		}
	}
	dev.info = newInfo
}

func readResponse(resp *http.Response) string {
	b, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		panic(err)
	}
	return string(b)
}

func (dev *Device) getSupportedServices(resp *http.Response) {
	doc := etree.NewDocument()

	data, _ := ioutil.ReadAll(resp.Body)

	if err := doc.ReadFromBytes(data); err != nil {
		//log.Println(err.Error())
		return
	}
	services := doc.FindElements("./Envelope/Body/GetCapabilitiesResponse/Capabilities/*/XAddr")
	for _, j := range services {
		dev.addEndpoint(j.Parent().Tag, j.Text())
	}

	extensionServices := doc.FindElements("./Envelope/Body/GetCapabilitiesResponse/Capabilities/Extension/*/XAddr")
	for _, j := range extensionServices {
		dev.addEndpoint(j.Parent().Tag, j.Text())
	}
}

// NewDevice function construct a ONVIF Device entity
func NewDevice(params DeviceParams) (*Device, error) {
	dev := new(Device)
	dev.params = params
	dev.endpoints = make(map[string]string)
	dev.addEndpoint("Device", "http://"+dev.params.Xaddr+"/onvif/device_service")

	if dev.params.HttpClient == nil {
		dev.params.HttpClient = new(http.Client)
	}
	dev.digestClient = NewDigestClient(dev.params.HttpClient, dev.params.Username, dev.params.Password)

	getCapabilities := device.GetCapabilities{Category: []onvif.CapabilityCategory{"All"}}

	resp, err := dev.CallMethod(getCapabilities)

	if err != nil || resp.StatusCode != http.StatusOK {
		return nil, errors.New("camera is not available at " + dev.params.Xaddr + " or it does not support ONVIF services")
	}

	dev.getSupportedServices(resp)
	return dev, nil
}

func (dev *Device) addEndpoint(Key, Value string) {
	//use lowCaseKey
	//make key having ability to handle Mixed Case for Different vendor devcie (e.g. Events EVENTS, events)
	lowCaseKey := strings.ToLower(Key)

	// Replace host with host from device params.
	if u, err := url.Parse(Value); err == nil {
		u.Host = dev.params.Xaddr
		Value = u.String()
	}

	dev.endpoints[lowCaseKey] = Value

	if lowCaseKey == strings.ToLower(MediaWebService) {
		// Media2 uses the same endpoint but different XML name space
		dev.endpoints[strings.ToLower(Media2WebService)] = Value
	}
}

// GetEndpoint returns specific ONVIF service endpoint address
func (dev *Device) GetEndpoint(name string) string {
	return dev.endpoints[name]
}

func (dev *Device) buildMethodSOAP(msg string) (gosoap.SoapMessage, error) {
	doc := etree.NewDocument()
	if err := doc.ReadFromString(msg); err != nil {
		//log.Println("Got error")

		return "", err
	}
	element := doc.Root()

	soap := gosoap.NewEmptySOAP()
	soap.AddBodyContent(element)

	return soap, nil
}

// getEndpoint functions get the target service endpoint in a better way
func (dev *Device) getEndpoint(endpoint string) (string, error) {

	// common condition, endpointMark in map we use this.
	if endpointURL, bFound := dev.endpoints[endpoint]; bFound {
		return endpointURL, nil
	}

	//but ,if we have endpoint like event、analytic
	//and sametime the Targetkey like : events、analytics
	//we use fuzzy way to find the best match url
	var endpointURL string
	for targetKey := range dev.endpoints {
		if strings.Contains(targetKey, endpoint) {
			endpointURL = dev.endpoints[targetKey]
			return endpointURL, nil
		}
	}
	return endpointURL, errors.New("target endpoint service not found")
}

// CallMethod functions call an method, defined <method> struct.
// You should use Authenticate method to call authorized requests.
func (dev Device) CallMethod(method interface{}) (*http.Response, error) {
	pkgPath := strings.Split(reflect.TypeOf(method).PkgPath(), "/")
	pkg := strings.ToLower(pkgPath[len(pkgPath)-1])

	endpoint, err := dev.getEndpoint(pkg)
	if err != nil {
		return nil, err
	}
	return dev.callMethodDo(endpoint, method)
}

// CallMethod functions call an method, defined <method> struct with authentication data
func (dev Device) callMethodDo(endpoint string, method interface{}) (*http.Response, error) {
	output, err := xml.MarshalIndent(method, "  ", "    ")
	if err != nil {
		return nil, err
	}

	soap, err := dev.buildMethodSOAP(string(output))
	if err != nil {
		return nil, err
	}

	soap.AddRootNamespaces(Xlmns)
	soap.AddAction()

	//Auth Handling
	if dev.params.Username != "" && dev.params.Password != "" {
		soap.AddWSSecurity(dev.params.Username, dev.params.Password)
	}

	servResp, err := networking.SendSoap(dev.params.HttpClient, endpoint, soap.String())
	if err != nil {
		// Close server response body to reuse the connection
		if servResp != nil {
			servResp.Body.Close()
		}
		servResp, err = networking.SendSoapWithDigest(dev.params.HttpClient, endpoint, soap.String(), dev.params.Username, dev.params.Password)
	}

	return servResp, err
}

func (dev *Device) GetDeviceParams() DeviceParams {
	return dev.params
}

func (dev *Device) GetEndpointByRequestStruct(requestStruct interface{}) (string, error) {
	pkgPath := strings.Split(reflect.TypeOf(requestStruct).Elem().PkgPath(), "/")
	pkg := strings.ToLower(pkgPath[len(pkgPath)-1])

	endpoint, err := dev.getEndpoint(pkg)
	if err != nil {
		return "", err
	}
	return endpoint, err
}

/*func (dev *Device) SendSoap(endpoint string, xmlRequestBody string) (resp *http.Response, err error) {
	soap := gosoap.NewEmptySOAP()
	soap.AddStringBodyContent(xmlRequestBody)
	soap.AddRootNamespaces(Xlmns)
	if dev.params.AuthMode == UsernameTokenAuth || dev.params.AuthMode == Both {
		soap.AddWSSecurity(dev.params.Username, dev.params.Password)
	}

	if dev.params.AuthMode == DigestAuth || dev.params.AuthMode == Both {
		resp, err = dev.digestClient.Do(http.MethodPost, endpoint, soap.String())
	} else {
		var req *http.Request
		req, err = createHttpRequest(http.MethodPost, endpoint, soap.String())
		if err != nil {
			return nil, err
		}
		resp, err = dev.params.HttpClient.Do(req)
	}
	return resp, err
}*/

// SendSoap POSTs the given body wrapped in a SOAP envelope.
func (dev Device) SendSoap(endpoint string, xmlRequestBody string) (*http.Response, error) {
	return dev.SendSoapWithHeader(endpoint, xmlRequestBody, "")
}

// SendSoapWithHeader is SendSoap plus arbitrary inner-Header XML —
// needed to echo WS-Addressing ReferenceParameters (with
// wsa:IsReferenceParameter="true") back to vendors like AXIS that
// identify pull-point subscriptions through them rather than the URL.
//
// xmlHeaderContent must be well-formed XML representing zero or more
// SOAP Header child elements (sibling top-level elements are
// supported; the spec lets each reference parameter be its own header
// block). The caller is responsible for escaping any externally
// sourced data inside it. Malformed XML returns an error before any
// request is made.
func (dev Device) SendSoapWithHeader(endpoint, xmlRequestBody, xmlHeaderContent string) (*http.Response, error) {
	soap := gosoap.NewEmptySOAP()
	soap.AddStringBodyContent(xmlRequestBody)
	soap.AddRootNamespaces(Xlmns)
	soap.AddAction()
	if err := addHeaderChildren(&soap, xmlHeaderContent); err != nil {
		return nil, err
	}
	if dev.params.Username != "" && dev.params.Password != "" {
		soap.AddWSSecurity(dev.params.Username, dev.params.Password)
	}

	servResp, err := networking.SendSoap(dev.params.HttpClient, endpoint, soap.String())
	if err != nil {
		if servResp != nil {
			servResp.Body.Close()
		}
		servResp, err = networking.SendSoapWithDigest(dev.params.HttpClient, endpoint, soap.String(), dev.params.Username, dev.params.Password)
	}
	return servResp, err
}

// addHeaderChildren wraps the fragment so etree can parse multi-root
// XML, then adds each top-level child as its own SOAP Header block.
// gosoap.AddStringHeaderContent only accepts a single root element.
func addHeaderChildren(soap *gosoap.SoapMessage, xmlHeaderContent string) error {
	if xmlHeaderContent == "" {
		return nil
	}
	doc := etree.NewDocument()
	if err := doc.ReadFromString("<wrap>" + xmlHeaderContent + "</wrap>"); err != nil {
		return fmt.Errorf("parse header content: %w", err)
	}
	wrap := doc.SelectElement("wrap")
	if wrap == nil {
		return errors.New("parse header content: missing wrap root")
	}
	for _, child := range wrap.ChildElements() {
		d := etree.NewDocument()
		d.SetRoot(child.Copy())
		s, err := d.WriteToString()
		if err != nil {
			return fmt.Errorf("serialise header child: %w", err)
		}
		if err := soap.AddStringHeaderContent(s); err != nil {
			return fmt.Errorf("add header child: %w", err)
		}
	}
	return nil
}

func createHttpRequest(httpMethod string, endpoint string, soap string) (req *http.Request, err error) {
	req, err = http.NewRequest(httpMethod, endpoint, bytes.NewBufferString(soap))
	if err != nil {
		return nil, err
	}
	req.Header.Set(ContentType, "application/soap+xml; charset=utf-8")
	return req, nil
}

func (dev *Device) CallOnvifFunction(serviceName, functionName string, data []byte) (interface{}, error) {
	function, err := FunctionByServiceAndFunctionName(serviceName, functionName)
	if err != nil {
		return nil, err
	}
	request, err := createRequest(function, data)
	if err != nil {
		return nil, fmt.Errorf("fail to create '%s' request for the web service '%s', %v", functionName, serviceName, err)
	}

	endpoint, err := dev.GetEndpointByRequestStruct(request)
	if err != nil {
		return nil, err
	}

	requestBody, err := xml.Marshal(request)
	if err != nil {
		return nil, err
	}
	xmlRequestBody := string(requestBody)

	servResp, err := dev.SendSoap(endpoint, xmlRequestBody)
	if err != nil {
		return nil, fmt.Errorf("fail to send the '%s' request for the web service '%s', %v", functionName, serviceName, err)
	}
	defer servResp.Body.Close()

	rsp, err := ioutil.ReadAll(servResp.Body)
	if err != nil {
		return nil, err
	}

	responseEnvelope, err := createResponse(function, rsp)
	if err != nil {
		return nil, fmt.Errorf("fail to create '%s' response for the web service '%s', %v", functionName, serviceName, err)
	}

	if servResp.StatusCode == http.StatusUnauthorized {
		return nil, fmt.Errorf("fail to verify the authentication for the function '%s' of web service '%s'. Onvif error: %s",
			functionName, serviceName, responseEnvelope.Body.Fault.String())
	} else if servResp.StatusCode == http.StatusBadRequest {
		return nil, fmt.Errorf("invalid request for the function '%s' of web service '%s'. Onvif error: %s",
			functionName, serviceName, responseEnvelope.Body.Fault.String())
	} else if servResp.StatusCode > http.StatusNoContent {
		return nil, fmt.Errorf("fail to execute the request for the function '%s' of web service '%s'. Onvif error: %s",
			functionName, serviceName, responseEnvelope.Body.Fault.String())
	}
	return responseEnvelope.Body.Content, nil
}

func createRequest(function Function, data []byte) (interface{}, error) {
	request := function.Request()
	if len(data) > 0 {
		err := json.Unmarshal(data, request)
		if err != nil {
			return nil, err
		}
	}
	return request, nil
}

func createResponse(function Function, data []byte) (*gosoap.SOAPEnvelope, error) {
	response := function.Response()
	responseEnvelope := gosoap.NewSOAPEnvelope(response)
	err := xml.Unmarshal(data, responseEnvelope)
	if err != nil {
		return nil, err
	}
	return responseEnvelope, nil
}

// SendGetSnapshotRequest sends the Get request to retrieve the snapshot from the Onvif camera
// The parameter url is come from the "GetSnapshotURI" command.
func (dev *Device) SendGetSnapshotRequest(url string) (resp *http.Response, err error) {
	soap := gosoap.NewEmptySOAP()
	soap.AddRootNamespaces(Xlmns)
	if dev.params.AuthMode == UsernameTokenAuth {
		soap.AddWSSecurity(dev.params.Username, dev.params.Password)
		var req *http.Request
		req, err = createHttpRequest(http.MethodGet, url, soap.String())
		if err != nil {
			return nil, err
		}
		// Basic auth might work for some camera
		req.SetBasicAuth(dev.params.Username, dev.params.Password)
		resp, err = dev.params.HttpClient.Do(req)

	} else if dev.params.AuthMode == DigestAuth || dev.params.AuthMode == Both {
		soap.AddWSSecurity(dev.params.Username, dev.params.Password)
		resp, err = dev.digestClient.Do(http.MethodGet, url, soap.String())

	} else {
		var req *http.Request
		req, err = createHttpRequest(http.MethodGet, url, soap.String())
		if err != nil {
			return nil, err
		}
		resp, err = dev.params.HttpClient.Do(req)
	}
	return resp, err
}
