package ssdp

import (
	"bufio"
	"bytes"
	"errors"
	"fmt"
	"io"
	"net"
	"net/http"
	"sync"

	"github.com/koron/go-ssdp/internal/multicast"
	"github.com/koron/go-ssdp/internal/ssdplog"
)

// Monitor monitors SSDP's alive and byebye messages.
type Monitor struct {
	Alive  AliveHandler
	Bye    ByeHandler
	Search SearchHandler

	conn *multicast.Conn
	wg   sync.WaitGroup
}

// Start starts to monitor SSDP messages.
func (m *Monitor) Start() error {
	conn, err := multicast.Listen(multicast.RecvAddrResolver)
	if err != nil {
		return err
	}
	ssdplog.Printf("monitoring on %s", conn.LocalAddr().String())
	m.conn = conn
	m.wg.Add(1)
	go func() {
		m.serve()
		m.wg.Done()
	}()
	return nil
}

func (m *Monitor) serve() error {
	// TODO: update listening interfaces of m.conn
	err := m.conn.ReadPackets(0, func(addr net.Addr, data []byte) error {
		msg := make([]byte, len(data))
		copy(msg, data)
		go m.handleRaw(addr, msg)
		return nil
	})
	if err != nil && !errors.Is(err, io.EOF) {
		return err
	}
	return nil
}

func (m *Monitor) handleRaw(addr net.Addr, raw []byte) error {
	// Add newline to workaround buggy SSDP responses
	if !bytes.HasSuffix(raw, endOfHeader) {
		raw = bytes.Join([][]byte{raw, endOfHeader}, nil)
	}
	if bytes.HasPrefix(raw, []byte("M-SEARCH ")) {
		return m.handleSearch(addr, raw)
	}
	if bytes.HasPrefix(raw, []byte("NOTIFY ")) {
		return m.handleNotify(addr, raw)
	}
	n := bytes.Index(raw, []byte("\r\n"))
	ssdplog.Printf("unexpected method: %q", string(raw[:n]))
	return nil
}

func (m *Monitor) handleNotify(addr net.Addr, raw []byte) error {
	req, err := http.ReadRequest(bufio.NewReader(bytes.NewReader(raw)))
	if err != nil {
		return err
	}
	switch nts := req.Header.Get("NTS"); nts {
	case "ssdp:alive":
		if req.Method != "NOTIFY" {
			return fmt.Errorf("unexpected method for %q: %s", "ssdp:alive", req.Method)
		}
		if h := m.Alive; h != nil {
			h(&AliveMessage{
				From:      addr,
				Type:      req.Header.Get("NT"),
				USN:       req.Header.Get("USN"),
				Location:  req.Header.Get("LOCATION"),
				Server:    req.Header.Get("SERVER"),
				rawHeader: req.Header,
			})
		}
	case "ssdp:byebye":
		if req.Method != "NOTIFY" {
			return fmt.Errorf("unexpected method for %q: %s", "ssdp:byebye", req.Method)
		}
		if h := m.Bye; h != nil {
			h(&ByeMessage{
				From:      addr,
				Type:      req.Header.Get("NT"),
				USN:       req.Header.Get("USN"),
				rawHeader: req.Header,
			})
		}
	default:
		return fmt.Errorf("unknown NTS: %s", nts)
	}
	return nil
}

func (m *Monitor) handleSearch(addr net.Addr, raw []byte) error {
	req, err := http.ReadRequest(bufio.NewReader(bytes.NewReader(raw)))
	if err != nil {
		return err
	}
	man := req.Header.Get("MAN")
	if man != `"ssdp:discover"` {
		return fmt.Errorf("unexpected MAN: %s", man)
	}
	if h := m.Search; h != nil {
		h(&SearchMessage{
			From:      addr,
			Type:      req.Header.Get("ST"),
			rawHeader: req.Header,
		})
	}
	return nil
}

// Close closes monitoring.
func (m *Monitor) Close() error {
	if m.conn != nil {
		m.conn.Close()
		m.conn = nil
		m.wg.Wait()
	}
	return nil
}

// AliveMessage represents SSDP's ssdp:alive message.
type AliveMessage struct {
	// From is a sender of this message
	From net.Addr

	// Type is a property of "NT"
	Type string

	// USN is a property of "USN"
	USN string

	// Location is a property of "LOCATION"
	Location string

	// Server is a property of "SERVER"
	Server string

	rawHeader http.Header
	maxAge    *int
}

// Header returns all properties in alive message.
func (m *AliveMessage) Header() http.Header {
	return m.rawHeader
}

// MaxAge extracts "max-age" value from "CACHE-CONTROL" property.
func (m *AliveMessage) MaxAge() int {
	if m.maxAge == nil {
		m.maxAge = new(int)
		*m.maxAge = extractMaxAge(m.rawHeader.Get("CACHE-CONTROL"), -1)
	}
	return *m.maxAge
}

// AliveHandler is handler of Alive message.
type AliveHandler func(*AliveMessage)

// ByeMessage represents SSDP's ssdp:byebye message.
type ByeMessage struct {
	// From is a sender of this message
	From net.Addr

	// Type is a property of "NT"
	Type string

	// USN is a property of "USN"
	USN string

	rawHeader http.Header
}

// Header returns all properties in bye message.
func (m *ByeMessage) Header() http.Header {
	return m.rawHeader
}

// ByeHandler is handler of Bye message.
type ByeHandler func(*ByeMessage)

// SearchMessage represents SSDP's ssdp:discover message.
type SearchMessage struct {
	From net.Addr
	Type string

	rawHeader http.Header
}

// Header returns all properties in search message.
func (s *SearchMessage) Header() http.Header {
	return s.rawHeader
}

// SearchHandler is handler of Search message.
type SearchHandler func(*SearchMessage)
