// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied.  See the License for the
// specific language governing permissions and limitations
// under the License.

package sniffer

import (
	"fmt"
	"io"
	"os"
	"runtime"
	"syscall"
	"time"

	"github.com/tsg/gopacket"
	"github.com/tsg/gopacket/layers"
	"github.com/tsg/gopacket/pcap"

	"github.com/elastic/beats/v7/libbeat/common/atomic"
	"github.com/elastic/beats/v7/libbeat/logp"

	"github.com/elastic/beats/v7/packetbeat/config"
)

// Sniffer provides packet sniffing capabilities, forwarding packets read
// to a Worker.
type Sniffer struct {
	config config.InterfacesConfig
	dumper *pcap.Dumper

	state atomic.Int32 // store snifferState

	// bpf filter
	filter string

	factory WorkerFactory
}

// WorkerFactory constructs a new worker instance for use with a Sniffer.
type WorkerFactory func(layers.LinkType) (Worker, error)

// Worker defines the callback interfaces a Sniffer instance will use
// to forward packets.
type Worker interface {
	OnPacket(data []byte, ci *gopacket.CaptureInfo)
}

type snifferHandle interface {
	gopacket.PacketDataSource

	LinkType() layers.LinkType
	Close()
}

// sniffer state values
const (
	snifferInactive = 0
	snifferClosing  = 1
	snifferActive   = 2
)

// New create a new Sniffer instance. Settings are validated in a best effort
// only, but no device is opened yet. Accessing and configuring the actual device
// is done by the Run method.
func New(
	testMode bool,
	filter string,
	factory WorkerFactory,
	interfaces config.InterfacesConfig,
) (*Sniffer, error) {
	s := &Sniffer{
		filter:  filter,
		config:  interfaces,
		factory: factory,
		state:   atomic.MakeInt32(snifferInactive),
	}

	logp.Debug("sniffer", "BPF filter: '%s'", filter)

	// pre-check and normalize configuration:
	// - resolve potential device name
	// - check for file output
	// - set some defaults
	if s.config.File != "" {
		logp.Debug("sniffer", "Reading from file: %s", s.config.File)

		if s.config.BpfFilter != "" {
			logp.Warn("Packet filters are not applied to pcap files.")
		}

		// we read file with the pcap provider
		s.config.Type = "pcap"
		s.config.Device = ""
	} else {
		// try to resolve device name (ignore error if testMode is enabled)
		if name, err := resolveDeviceName(s.config.Device); err != nil {
			if !testMode {
				return nil, err
			}
		} else {
			s.config.Device = name
			if name == "any" && !deviceAnySupported {
				return nil, fmt.Errorf("any interface is not supported on %s", runtime.GOOS)
			}

			if s.config.Snaplen == 0 {
				s.config.Snaplen = 65535
			}
			if s.config.BufferSizeMb <= 0 {
				s.config.BufferSizeMb = 24
			}

			if t := s.config.Type; t == "autodetect" || t == "" {
				s.config.Type = "pcap"
			}
			logp.Debug("sniffer", "Sniffer type: %s device: %s", s.config.Type, s.config.Device)
		}
	}

	err := validateConfig(filter, &s.config)
	if err != nil {
		return nil, err
	}

	return s, nil
}

// Run opens the sniffing device and processes packets being read from that device.
// Worker instances are instantiated as needed.
func (s *Sniffer) Run() error {
	var (
		counter = 0
		dumper  *pcap.Dumper
	)

	handle, err := s.open()
	if err != nil {
		return fmt.Errorf("Error starting sniffer: %s", err)
	}
	defer handle.Close()

	if s.config.Dumpfile != "" {
		dumper, err = openDumper(s.config.Dumpfile, handle.LinkType())
		if err != nil {
			return err
		}

		defer dumper.Close()
	}

	worker, err := s.factory(handle.LinkType())
	if err != nil {
		return err
	}

	// Mark inactive sniffer as active. In case of the sniffer/packetbeat closing
	// before/while Run is executed, the state will be snifferClosing.
	// => return if state is already snifferClosing.
	if !s.state.CAS(snifferInactive, snifferActive) {
		return nil
	}
	defer s.state.Store(snifferInactive)

	for s.state.Load() == snifferActive {
		if s.config.OneAtATime {
			fmt.Println("Press enter to read packet")
			fmt.Scanln()
		}

		data, ci, err := handle.ReadPacketData()
		if err == pcap.NextErrorTimeoutExpired || err == syscall.EINTR {
			logp.Debug("sniffer", "Interrupted")
			continue
		}

		if err != nil {
			// ignore EOF, if sniffer was driven from file
			if err == io.EOF && s.config.File != "" {
				return nil
			}

			s.state.Store(snifferInactive)
			return fmt.Errorf("Sniffing error: %s", err)
		}

		if len(data) == 0 {
			// Empty packet, probably timeout from afpacket
			continue
		}

		if dumper != nil {
			dumper.WritePacketData(data, ci)
		}

		counter++
		logp.Debug("sniffer", "Packet number: %d", counter)
		worker.OnPacket(data, &ci)
	}

	return nil
}

func (s *Sniffer) open() (snifferHandle, error) {
	if s.config.File != "" {
		return newFileHandler(s.config.File, s.config.TopSpeed, s.config.Loop)
	}

	switch s.config.Type {
	case "pcap":
		return openPcap(s.filter, &s.config)
	case "af_packet":
		return openAFPacket(s.filter, &s.config)
	default:
		return nil, fmt.Errorf("Unknown sniffer type: %s", s.config.Type)
	}
}

// Stop marks a sniffer as stopped. The Run method will return once the stop
// signal has been given.
func (s *Sniffer) Stop() error {
	s.state.Store(snifferClosing)
	return nil
}

func validateConfig(filter string, cfg *config.InterfacesConfig) error {
	if cfg.File == "" {
		if err := validatePcapFilter(filter); err != nil {
			return err
		}
	}

	switch cfg.Type {
	case "pcap":
		return validatePcapConfig(cfg)
	case "af_packet":
		return validateAfPacketConfig(cfg)
	default:
		return fmt.Errorf("Unknown sniffer type: %s", cfg.Type)
	}
}

func validatePcapConfig(cfg *config.InterfacesConfig) error {
	return nil
}

func validateAfPacketConfig(cfg *config.InterfacesConfig) error {
	_, _, _, err := afpacketComputeSize(cfg.BufferSizeMb, cfg.Snaplen, os.Getpagesize())
	return err
}

func validatePcapFilter(expr string) error {
	if expr == "" {
		return nil
	}

	// Open a dummy pcap handle to compile the filter
	p, err := pcap.OpenDead(layers.LinkTypeEthernet, 65535)
	if err != nil {
		return fmt.Errorf("OpenDead: %s", err)
	}

	defer p.Close()

	_, err = p.NewBPF(expr)
	if err != nil {
		return fmt.Errorf("invalid filter '%s': %v", expr, err)
	}
	return nil
}

func openPcap(filter string, cfg *config.InterfacesConfig) (snifferHandle, error) {
	snaplen := int32(cfg.Snaplen)
	timeout := 500 * time.Millisecond
	h, err := pcap.OpenLive(cfg.Device, snaplen, true, timeout)
	if err != nil {
		return nil, err
	}

	err = h.SetBPFFilter(filter)
	if err != nil {
		h.Close()
		return nil, err
	}

	return h, nil
}

func openAFPacket(filter string, cfg *config.InterfacesConfig) (snifferHandle, error) {
	szFrame, szBlock, numBlocks, err := afpacketComputeSize(cfg.BufferSizeMb, cfg.Snaplen, os.Getpagesize())
	if err != nil {
		return nil, err
	}

	timeout := 500 * time.Millisecond
	h, err := newAfpacketHandle(cfg.Device, szFrame, szBlock, numBlocks, timeout, cfg.EnableAutoPromiscMode)
	if err != nil {
		return nil, err
	}

	err = h.SetBPFFilter(filter)
	if err != nil {
		h.Close()
		return nil, err
	}

	return h, nil
}

func openDumper(file string, linkType layers.LinkType) (*pcap.Dumper, error) {
	p, err := pcap.OpenDead(linkType, 65535)
	if err != nil {
		return nil, err
	}

	return p.NewDumper(file)
}
