// Package ldif contains an  LDIF parser and marshaller (RFC 2849).
package ldif

import (
	"bufio"
	"bytes"
	"encoding/base64"
	"errors"
	"fmt"
	"io"
	"iter"
	"net/url"
	"os"
	"strconv"
	"strings"

	"github.com/go-ldap/ldap/v3"
)

// Entry is one entry in the LDIF
type Entry struct {
	Entry  *ldap.Entry
	Add    *ldap.AddRequest
	Del    *ldap.DelRequest
	Modify *ldap.ModifyRequest
}

// The LDIF struct is used for parsing an LDIF. The Controls
// is used to tell the parser to ignore any controls found
// when parsing (default: false to ignore the controls).
// FoldWidth is used for the line lenght when marshalling.
type LDIF struct {
	Entries    []*Entry
	Version    int
	changeType string
	FoldWidth  int
	Controls   bool
	firstEntry bool
}

// The ParseError holds the error message and the line in the ldif
// where the error occurred.
type ParseError struct {
	Line    int
	Message string
}

// Error implements the error interface
func (e *ParseError) Error() string {
	return fmt.Sprintf("Error in line %d: %s", e.Line, e.Message)
}

var cr byte = '\x0D'
var lf byte = '\x0A'
var sep = string([]byte{cr, lf})
var comment byte = '#'
var space byte = ' '
var spaces = string(space)

// Parse wraps Unmarshal to parse an LDIF from a string
func Parse(str string) (l *LDIF, err error) {
	buf := bytes.NewBuffer([]byte(str))
	l = &LDIF{}
	err = Unmarshal(buf, l)
	return
}

// ParseWithControls wraps Unmarshal to parse an LDIF from
// a string, controls are added to change records
func ParseWithControls(str string) (l *LDIF, err error) {
	buf := bytes.NewBuffer([]byte(str))
	l = &LDIF{Controls: true}
	err = Unmarshal(buf, l)
	return
}

// Unmarshal parses the LDIF from the given io.Reader into the LDIF struct.
// The caller is responsible for closing the io.Reader if that is
// needed.
func Unmarshal(r io.Reader, l *LDIF) (err error) {
	for entry, err := range UnmarshalEntries(r, l) {
		if err != nil {
			return err
		}
		l.Entries = append(l.Entries, entry)
	}
	return nil
}

// UnmarshalEntries parses the LDIF from the given io.Reader and yield the individual entries during an iteration.
// The caller is responsible for closing the io.Reader if that is needed.
func UnmarshalEntries(r io.Reader, l *LDIF) iter.Seq2[*Entry, error] {
	return func(yield func(*Entry, error) bool) {
		for e, err := range unmarshalEntries(r, l) {
			if err == nil && e == nil {
				// I couldn't find use-case where the entry and the error is both nil,
				// but I saw helper functions to help avoid them,
				// so I added this logic here, to skip nil entries
				// while preserving Unmarshal's original behaviour.
				continue
			}
			if !yield(e, err) {
				return
			}
		}
	}
}

func unmarshalEntries(r io.Reader, l *LDIF) iter.Seq2[*Entry, error] {
	return func(yield func(*Entry, error) bool) {
		if r == nil {
			yield(nil, &ParseError{Line: 0, Message: "No reader present"})
			return
		}

		curLine := 0
		l.Version = 0
		l.changeType = ""
		isComment := false

		reader := bufio.NewReader(r)

		var lines []string
		var line, nextLine string
		l.firstEntry = true

		for {
			curLine++
			var err error
			nextLine, err = reader.ReadString(lf)
			nextLine = strings.TrimRight(nextLine, sep)

			switch err {
			case nil, io.EOF:
				switch len(nextLine) {
				case 0:
					if len(line) == 0 && err == io.EOF {
						return
					}
					if len(line) == 0 && len(lines) == 0 {
						continue
					}
					lines = append(lines, line)
					entry, perr := l.parseEntry(lines)
					if perr != nil {
						yield(nil, &ParseError{Line: curLine, Message: perr.Error()})
						return
					}
					if !yield(entry, nil) {
						return
					}
					line = ""
					lines = []string{}
					if err == io.EOF {
						return
					}
				default:
					switch nextLine[0] {
					case comment:
						isComment = true
						continue

					case space:
						if isComment {
							continue
						}
						line += nextLine[1:]
						continue

					default:
						isComment = false
						if len(line) != 0 {
							lines = append(lines, line)
						}
						line = nextLine
						continue
					}
				}
			default:
				yield(nil, &ParseError{Line: curLine, Message: err.Error()})
				return
			}
		}
	}
}

func (l *LDIF) parseEntry(lines []string) (entry *Entry, err error) {
	if len(lines) == 0 {
		return nil, errors.New("empty entry?")
	}

	if l.firstEntry && strings.HasPrefix(lines[0], "version:") {
		l.firstEntry = false
		line := strings.TrimLeft(lines[0][8:], spaces)
		if l.Version, err = strconv.Atoi(line); err != nil {
			return nil, err
		}

		if l.Version != 1 {
			return nil, errors.New("Invalid version spec " + string(line))
		}

		l.Version = 1
		if len(lines) == 1 {
			return nil, nil
		}
		lines = lines[1:]
	}
	l.firstEntry = false

	if len(lines) == 0 {
		return nil, nil
	}

	if !strings.HasPrefix(lines[0], "dn:") {
		return nil, errors.New("missing 'dn:'")
	}
	_, val, err := l.parseLine(lines[0])
	if err != nil {
		return nil, err
	}
	dn := val

	if len(lines) == 1 {
		return nil, errors.New("only a dn: line")
	}
	lines = lines[1:]

	var controls []ldap.Control
	controls, lines, err = l.parseControls(lines)
	if err != nil {
		return nil, err
	}

	if strings.HasPrefix(lines[0], "changetype:") {
		_, val, err := l.parseLine(lines[0])
		if err != nil {
			return nil, err
		}
		l.changeType = val
		if len(lines) > 1 {
			lines = lines[1:]
		}
	}
	switch l.changeType {
	case "":
		if len(controls) != 0 {
			return nil, errors.New("controls found without changetype")
		}
		attrs, err := l.parseAttrs(lines)
		if err != nil {
			return nil, err
		}
		return &Entry{Entry: ldap.NewEntry(dn, attrs)}, nil

	case "add":
		attrs, err := l.parseAttrs(lines)
		if err != nil {
			return nil, err
		}
		// FIXME: controls for add - see https://github.com/go-ldap/ldap/issues/81
		add := ldap.NewAddRequest(dn, controls)
		for attr, vals := range attrs {
			add.Attribute(attr, vals)
		}
		return &Entry{Add: add}, nil

	case "delete":
		if len(lines) > 1 {
			return nil, errors.New("no attributes allowed for changetype delete")
		}
		return &Entry{Del: ldap.NewDelRequest(dn, controls)}, nil

	case "modify":
		// FIXME: controls for modify - see https://github.com/go-ldap/ldap/issues/81
		mod := ldap.NewModifyRequest(dn, controls)
		var op, attribute string
		var values []string
		if lines[len(lines)-1] != "-" {
			return nil, errors.New("modify request does not close with a single dash")
		}

		for i := 0; i < len(lines); i++ {
			if lines[i] == "-" {
				switch op {
				case "":
					return nil, fmt.Errorf("empty operation")
				case "add":
					mod.Add(attribute, values)
					op = ""
					attribute = ""
					values = nil
				case "replace":
					mod.Replace(attribute, values)
					op = ""
					attribute = ""
					values = nil
				case "delete":
					mod.Delete(attribute, values)
					op = ""
					attribute = ""
					values = nil
				default:
					return nil, fmt.Errorf("invalid operation %s in modify request", op)
				}
				continue
			}
			attr, val, err := l.parseLine(lines[i])
			if err != nil {
				return nil, err
			}
			if op == "" {
				op = attr
				attribute = val
			} else {
				if attr != attribute {
					return nil, fmt.Errorf("invalid attribute %s in %s request for %s", attr, op, attribute)
				}
				values = append(values, val)
			}
		}
		return &Entry{Modify: mod}, nil

	case "moddn", "modrdn":
		return nil, fmt.Errorf("unsupported changetype %s", l.changeType)

	default:
		return nil, fmt.Errorf("invalid changetype %s", l.changeType)
	}
}

func (l *LDIF) parseAttrs(lines []string) (map[string][]string, error) {
	attrs := make(map[string][]string)
	for i := 0; i < len(lines); i++ {
		attr, val, err := l.parseLine(lines[i])
		if err != nil {
			return nil, err
		}
		attrs[attr] = append(attrs[attr], val)
	}
	return attrs, nil
}

func (l *LDIF) parseLine(line string) (attr, val string, err error) {
	off := 0
	for len(line) > off && line[off] != ':' {
		off++
		if off >= len(line) {
			err = fmt.Errorf("Missing : in line `%s`", line)
			return
		}
	}
	if off == len(line) {
		err = fmt.Errorf("Missing : in the line `%s`", line)
		return
	}

	if off > len(line)-2 {
		err = errors.New("empty value")
		// FIXME: this is allowed for some attributes, e.g. seeAlso
		return
	}

	attr = line[0:off]
	if err = validAttr(attr); err != nil {
		attr = ""
		val = ""
		return
	}

	switch line[off+1] {
	case ':':
		val, err = decodeBase64(strings.TrimLeft(line[off+2:], spaces))
		if err != nil {
			return
		}

	case '<':
		val, err = readURLValue(strings.TrimLeft(line[off+2:], spaces))
		if err != nil {
			return
		}

	default:
		val = strings.TrimLeft(line[off+1:], spaces)
	}

	return
}

func (l *LDIF) parseControls(lines []string) ([]ldap.Control, []string, error) {
	var controls []ldap.Control
	for {
		if !strings.HasPrefix(lines[0], "control:") {
			break
		}
		if !l.Controls {
			if len(lines) == 1 {
				return nil, nil, errors.New("only controls found")
			}
			lines = lines[1:]
			continue
		}

		_, val, err := l.parseLine(lines[0])
		if err != nil {
			return nil, nil, err
		}

		var oid, ctrlValue string
		criticality := false

		parts := strings.SplitN(val, " ", 3)
		if err = validOID(parts[0]); err != nil {
			return nil, nil, fmt.Errorf("%s is not a valid oid: %s", oid, err)
		}
		oid = parts[0]

		if len(parts) > 1 {
			switch parts[1] {
			case "true":
				criticality = true
				if len(parts) > 2 {
					parts[1] = parts[2]
					parts = parts[0:2]
				}
			case "false":
				criticality = false
				if len(parts) > 2 {
					parts[1] = parts[2]
					parts = parts[0:2]
				}
			}
		}
		if len(parts) == 2 {
			ctrlValue = parts[1]
		}
		if ctrlValue == "" {
			switch oid {
			case ldap.ControlTypeManageDsaIT:
				controls = append(controls, &ldap.ControlManageDsaIT{Criticality: criticality})
			default:
				return nil, nil, fmt.Errorf("unsupported control found: %s", oid)
			}
		} else {
			switch ctrlValue[0] { // where is this documented?
			case ':':
				if len(ctrlValue) == 1 {
					return nil, nil, errors.New("missing value for base64 encoded control value")
				}
				ctrlValue, err = decodeBase64(strings.TrimLeft(ctrlValue[1:], spaces))
				if err != nil {
					return nil, nil, err
				}
				if ctrlValue == "" {
					return nil, nil, errors.New("base64 decoded to empty value")
				}

			case '<':
				if len(ctrlValue) == 1 {
					return nil, nil, errors.New("missing value for url control value")
				}
				ctrlValue, err = readURLValue(strings.TrimLeft(ctrlValue[1:], spaces))
				if err != nil {
					return nil, nil, err
				}
				if ctrlValue == "" {
					return nil, nil, errors.New("url resolved to an empty value")
				}
			}
			// TODO:
			// convert ctrlValue to *ber.Packet and decode with something like
			//   ctrl := ldap.DecodeControl()
			// ... FIXME: the controls need a Decode() interface
			// so we can just do a
			//   ctrl := ldap.ControlByOID(oid) // returns an empty &ControlSomething{}
			//   ctrl.Decode((*ber.Packet)(ctrlValue))
			//   ctrl.Criticality = criticality
			// that should be usable in github.com/go-ldap/ldap/control.go also
			// to decode the incoming control
			// controls = append(controls, ctrl)
			return nil, nil, fmt.Errorf("controls with values are not supported, oid: %s", oid)
		}

		if len(lines) == 1 {
			return nil, nil, errors.New("only controls found")
		}
		lines = lines[1:]
	}
	return controls, lines, nil
}

func readURLValue(val string) (string, error) {
	u, err := url.Parse(val)
	if err != nil {
		return "", fmt.Errorf("failed to parse URL: %s", err)
	}
	if u.Scheme != "file" {
		return "", fmt.Errorf("unsupported URL scheme %s", u.Scheme)
	}
	data, err := os.ReadFile(toPath(u))
	if err != nil {
		return "", fmt.Errorf("failed to read %s: %s", u.Path, err)
	}
	val = string(data) // FIXME: safe?
	return val, nil
}

func decodeBase64(enc string) (string, error) {
	dec := make([]byte, base64.StdEncoding.DecodedLen(len([]byte(enc))))
	n, err := base64.StdEncoding.Decode(dec, []byte(enc))
	if err != nil {
		return "", err
	}
	return string(dec[:n]), nil
}

func validOID(oid string) error {
	lastDot := true
	for _, c := range oid {
		switch {
		case c == '.' && lastDot:
			return errors.New("OID with at least 2 consecutive dots")
		case c == '.':
			lastDot = true
		case c >= '0' && c <= '9':
			lastDot = false
		default:
			return errors.New("Invalid character in OID")
		}
	}
	return nil
}

func validAttr(attr string) error {
	if len(attr) == 0 {
		return errors.New("empty attribute name")
	}
	switch {
	case attr[0] >= 'A' && attr[0] <= 'Z':
		// A-Z
	case attr[0] >= 'a' && attr[0] <= 'z':
		// a-z
	default:
		if attr[0] >= '0' && attr[0] <= '9' {
			return validOID(attr)
		}
		return errors.New("invalid first character in attribute")
	}
	for i := 1; i < len(attr); i++ {
		c := attr[i]
		switch {
		case c >= '0' && c <= '9':
		case c >= 'A' && c <= 'Z':
		case c >= 'a' && c <= 'z':
		case c == '-':
		case c == ';':
		default:
			return errors.New("invalid character in attribute name")
		}
	}
	return nil
}

// AllEntries returns all *ldap.Entries in the LDIF
func (l *LDIF) AllEntries() (entries []*ldap.Entry) {
	for _, entry := range l.Entries {
		if entry.Entry != nil {
			entries = append(entries, entry.Entry)
		}
	}
	return entries
}
