package gophermap

import (
	"bufio"
	"bytes"
	"errors"
	"fmt"
	"io"
	"net/url"
	"os"
	"os/exec"
	"path/filepath"
	"sort"
	"strconv"
	"strings"

	"tildegit.org/tjp/sliderule/gopher"
	"tildegit.org/tjp/sliderule/internal"
	"tildegit.org/tjp/sliderule/internal/types"
)

// ExtendedMapDocument is a gophermap doc with gophernicus's extensions
//
// These are documented at: gopher://gopher.gophernicus.org/0/docs/README.Gophermap
type ExtendedMapDocument struct {
	Lines    []gopher.MapItem
	Location *url.URL
}

// ParseExtended parses a gophermap document including gophernicus extensions.
func ParseExtended(input io.Reader, location *url.URL) (*ExtendedMapDocument, error) {
	rdr := bufio.NewReader(input)
	doc := &ExtendedMapDocument{Location: location}

outer:
	for num := 1; ; num += 1 {
		var item gopher.MapItem
		var spl []string
		line, err := rdr.ReadString('\n')
		isEOF := errors.Is(err, io.EOF)
		if err != nil && !isEOF {
			return nil, err
		}
		line = strings.TrimRight(line, "\r\n")

		if len(line) > 0 {
			switch line[0] {

			case '#':
				doc.Lines = append(doc.Lines, gopher.MapItem{
					Type:    CommentType,
					Display: strings.TrimPrefix(line[1:], " "),
				})
				goto next
			case '!':
				doc.Lines = append(doc.Lines, gopher.MapItem{
					Type:    TitleType,
					Display: line[1:],
				})
				goto next
			case '-':
				doc.Lines = append(doc.Lines, gopher.MapItem{
					Type:     HiddenType,
					Selector: line[1:],
				})
				goto next
			case ':':
				doc.Lines = append(doc.Lines, gopher.MapItem{
					Type:    ExtensionType,
					Display: line[1:],
				})
				goto next
			case '=':
				doc.Lines = append(doc.Lines, gopher.MapItem{
					Type:     InclusionType,
					Selector: line[1:],
				})
				goto next
			}
		}

		switch line {
		case "~":
			doc.Lines = append(doc.Lines, gopher.MapItem{Type: UserListType})
			goto next
		case "%":
			doc.Lines = append(doc.Lines, gopher.MapItem{Type: VHostListType})
			goto next
		case ".":
			doc.Lines = append(doc.Lines, gopher.MapItem{Type: EndDocType})
			break outer
		case "*":
			doc.Lines = append(doc.Lines, gopher.MapItem{Type: DirListType})
			break outer
		}

		if !strings.Contains(line, "\t") {
			doc.Lines = append(doc.Lines, gopher.MapItem{
				Type:     gopher.InfoMessageType,
				Display:  line,
				Selector: location.Path,
				Hostname: location.Hostname(),
				Port:     location.Port(),
			})
			goto next
		}

		item = gopher.MapItem{Type: types.Status(line[0])}

		spl = strings.Split(line[1:], "\t")
		if len(spl) != 4 {
			return doc, InvalidLine(num)
		}
		item.Display = string(spl[0])
		item.Selector = string(spl[1])
		item.Hostname = string(spl[2])
		item.Port = string(spl[3])
		if _, err = strconv.Atoi(item.Port); err != nil {
			return doc, InvalidLine(num)
		}
		doc.Lines = append(doc.Lines, item)

	next:
		if isEOF {
			break
		}
	}

	return doc, nil
}

// Extensions to gopher types from Gophernicus.
const (
	// CommentType is omitted from generated compatible gophermaps.
	CommentType types.Status = '#'

	// TitleType defines the title of a gophermap document.
	TitleType types.Status = '!'

	// HiddenType hides a link from the generated compatible gophermap.
	HiddenType types.Status = '-'

	// ExtensionType defines the gopher type to use for files in the current directory with a given extension.
	ExtensionType types.Status = ':'

	// UserListType generates a list of users with valid ~/public_gopher directories.
	UserListType types.Status = '~'

	// VHostListType generates a listing of virtual hosts.
	//
	// It is not supported in sliderule.
	VHostListType types.Status = '%'

	// InclusionType causes another gophermap to be included at this location.
	InclusionType types.Status = '='

	// DirListType stops parsing the current file and ends the generated gophermap with a listing of the current directory.
	DirListType types.Status = '*'

	// EndDocType ends the current gophermap file.
	EndDocType types.Status = '.'
)

type FileSystemSettings struct {
	ParseExtended bool
	Exec          bool
	ListUsers     bool
	DirMaps       []string
	DirTag        string
}

// Compatible builds a standards-compliant gophermap from the current extended menu.
func (edoc *ExtendedMapDocument) Compatible(cwd string, settings FileSystemSettings) (gopher.MapDocument, string, error) {
	doc := gopher.MapDocument{}

	title := ""
	hidden := map[string]struct{}{}
	extensions := map[string]types.Status{}

	for num, item := range edoc.Lines {
		switch item.Type {
		case CommentType:
		case TitleType:
			title = item.Display
		case HiddenType:
			hidden[item.Selector] = struct{}{}
		case ExtensionType:
			from, to, found := strings.Cut(item.Display, "=")
			if !found {
				return nil, "", InvalidLine(num + 1)
			}
			extensions[from] = types.Status(to[0])
		case UserListType:
			if !settings.ListUsers {
				doc = append(doc, gopher.MapItem{
					Type:     gopher.InfoMessageType,
					Display:  "~",
					Selector: edoc.Location.Path,
					Hostname: edoc.Location.Hostname(),
					Port:     edoc.Location.Port(),
				})
			}

			users, err := internal.ListUsersWithHomeSubdir("public_gopher", 4)
			if err != nil {
				return nil, "", err
			}
			sort.Strings(users)
			for _, user := range users {
				doc = append(doc, gopher.MapItem{
					Type:     gopher.MenuType,
					Display:  "~" + user,
					Selector: "/~" + user,
					Hostname: edoc.Location.Hostname(),
					Port:     edoc.Location.Port(),
				})
			}
		case VHostListType:
			return nil, "", errors.New("Virtual host listings '%' are not supported")
		case InclusionType:
			subEdoc, err := openExtended(item.Selector, edoc.Location, settings)
			if err != nil {
				return nil, "", err
			}

			lines, _, err := subEdoc.Compatible(filepath.Dir(item.Selector), settings)
			if err != nil {
				return nil, "", err
			}
			doc = append(doc, lines...)
		case DirListType:
			dirlist, err := listDir(cwd, edoc.Location, settings, hidden, extensions)
			if err != nil {
				return nil, "", err
			}

			doc = append(doc, dirlist...)

			break
		case EndDocType:
			break
		default:
			doc = append(doc, item)
		}
	}

	return doc, title, nil
}

func openExtended(path string, location *url.URL, settings FileSystemSettings) (*ExtendedMapDocument, error) {
	file, err := os.Open(path)
	if err != nil {
		return nil, err
	}
	defer func() {
		_ = file.Close()
	}()

	if settings.Exec {
		info, err := file.Stat()
		if err != nil {
			return nil, err
		}
		m := info.Mode()
		if m.IsRegular() && m&5 == 5 {
			cmd := exec.Command(path)
			cmd.Env = []string{}
			cmd.Dir = filepath.Dir(path)

			outbuf := &bytes.Buffer{}
			cmd.Stdout = outbuf

			if err := cmd.Run(); err != nil {
				var exErr *exec.ExitError
				if errors.As(err, &exErr) {
					return nil, fmt.Errorf("Inclusion exec returned exit code %d", exErr.ExitCode())
				}
				return nil, err
			}

			return ParseExtended(outbuf, location)
		}
	}

	return ParseExtended(file, location)
}