// Package identd implements an Identification Protocol server.
//
// The Identification Protocol is defined in RFC 1413.
package identd

import (
	"bufio"
	"fmt"
	"net"
	"strconv"
	"strings"
	"sync"
	"time"
)

var identdTimeout = 10 * time.Second

type identKey struct {
	remoteHost string
	remotePort int
	localPort  int
}

func newIdentKey(remoteAddr, localAddr string) (*identKey, error) {
	remoteHost, remotePort, err := splitHostPort(remoteAddr)
	if err != nil {
		return nil, err
	}
	_, localPort, err := splitHostPort(localAddr)
	if err != nil {
		return nil, err
	}
	return &identKey{
		remoteHost: remoteHost,
		remotePort: remotePort,
		localPort:  localPort,
	}, nil
}

func splitHostPort(addr string) (host string, port int, err error) {
	host, portStr, err := net.SplitHostPort(addr)
	if err != nil {
		return "", 0, err
	}
	port, err = strconv.Atoi(portStr)
	return host, port, err
}

// Identd implements an ident server, as described in RFC 1413.
type Identd struct {
	entries map[identKey]string
	lock    sync.RWMutex
}

func New() *Identd {
	return &Identd{entries: make(map[identKey]string)}
}

func (s *Identd) Store(remoteAddr, localAddr, ident string) {
	k, err := newIdentKey(remoteAddr, localAddr)
	if err != nil {
		return
	}
	s.lock.Lock()
	s.entries[*k] = ident
	s.lock.Unlock()
}

func (s *Identd) Delete(remoteAddr, localAddr string) {
	k, err := newIdentKey(remoteAddr, localAddr)
	if err != nil {
		return
	}
	s.lock.Lock()
	delete(s.entries, *k)
	s.lock.Unlock()
}

func (s *Identd) Serve(ln net.Listener) error {
	for {
		conn, err := ln.Accept()
		if err != nil {
			return fmt.Errorf("failed to accept connection: %v", err)
		}

		go s.handle(conn)
	}
}

func (s *Identd) handle(c net.Conn) {
	defer c.Close()

	remoteHost, _, err := net.SplitHostPort(c.RemoteAddr().String())
	if err != nil {
		return
	}

	scanner := bufio.NewScanner(c)

	// We only read to read lines with two port numbers
	var buf [512]byte
	scanner.Buffer(buf[:], len(buf))

	for {
		c.SetDeadline(time.Now().Add(identdTimeout))
		if !scanner.Scan() {
			break
		}
		l := scanner.Text()

		localPort, remotePort, err := parseIdentQuery(l)
		if err != nil {
			fmt.Fprintf(c, "%s : ERROR : INVALID-PORT\r\n", l)
			break
		}

		k := identKey{
			remoteHost: remoteHost,
			remotePort: remotePort,
			localPort:  localPort,
		}

		s.lock.RLock()
		ident := s.entries[k]
		s.lock.RUnlock()

		if ident == "" {
			fmt.Fprintf(c, "%s : ERROR : NO-USER\r\n", l)
			break
		}

		// The "OTHER" operating system may be rejected by IRC servers, because
		// it may be used when the ident string isn't stable. Use "UNKNOWN"
		// from RFC 1340 instead.
		fmt.Fprintf(c, "%s : USERID : UNKNOWN : %s\r\n", l, ident)
	}
}

func parseIdentQuery(l string) (localPort, remotePort int, err error) {
	parts := strings.SplitN(l, ",", 2)
	if len(parts) != 2 {
		return 0, 0, fmt.Errorf("expected two ports")
	}
	localStr, remoteStr := strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1])
	if localPort, err = strconv.Atoi(localStr); err != nil {
		return 0, 0, err
	}
	if remotePort, err = strconv.Atoi(remoteStr); err != nil {
		return 0, 0, err
	}
	if localPort <= 0 || remotePort <= 0 {
		return 0, 0, fmt.Errorf("invalid port")
	}
	return localPort, remotePort, nil
}
