// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause

//go:build linux

package linuxfw

import (
	"errors"
	"fmt"
	"os"
	"slices"
	"strconv"
	"strings"
)

type fakeIPTables struct {
	n map[string][]string
}

type fakeRule struct {
	table, chain string
	args         []string
}

func newFakeIPTables() *fakeIPTables {
	return &fakeIPTables{
		n: map[string][]string{
			"filter/INPUT":      nil,
			"filter/OUTPUT":     nil,
			"filter/FORWARD":    nil,
			"nat/PREROUTING":    nil,
			"nat/OUTPUT":        nil,
			"nat/POSTROUTING":   nil,
			"mangle/FORWARD":    nil,
			"mangle/PREROUTING": nil,
			"mangle/OUTPUT":     nil,
		},
	}
}

func (n *fakeIPTables) Insert(table, chain string, pos int, args ...string) error {
	k := table + "/" + chain
	if rules, ok := n.n[k]; ok {
		if pos > len(rules)+1 {
			return fmt.Errorf("bad position %d in %s", pos, k)
		}
		rules = append(rules, "")
		copy(rules[pos:], rules[pos-1:])
		rules[pos-1] = strings.Join(args, " ")
		n.n[k] = rules
	} else {
		return fmt.Errorf("unknown table/chain %s", k)
	}
	return nil
}

func (n *fakeIPTables) Append(table, chain string, args ...string) error {
	k := table + "/" + chain
	return n.Insert(table, chain, len(n.n[k])+1, args...)
}

func (n *fakeIPTables) Exists(table, chain string, args ...string) (bool, error) {
	k := table + "/" + chain
	if rules, ok := n.n[k]; ok {
		if slices.Contains(rules, strings.Join(args, " ")) {
			return true, nil
		}
		return false, nil
	} else {
		return false, fmt.Errorf("unknown table/chain %s", k)
	}
}

func (n *fakeIPTables) Delete(table, chain string, args ...string) error {
	k := table + "/" + chain
	if rules, ok := n.n[k]; ok {
		for i, rule := range rules {
			if rule == strings.Join(args, " ") {
				rules = append(rules[:i], rules[i+1:]...)
				n.n[k] = rules
				return nil
			}
		}
		return errors.New("exitcode:1")
	} else {
		return fmt.Errorf("unknown table/chain %s", k)
	}
}

func (n *fakeIPTables) List(table, chain string) ([]string, error) {
	k := table + "/" + chain
	if rules, ok := n.n[k]; ok {
		return rules, nil
	} else {
		return nil, fmt.Errorf("unknown table/chain %s", k)
	}
}

func (n *fakeIPTables) ClearChain(table, chain string) error {
	k := table + "/" + chain
	if _, ok := n.n[k]; ok {
		n.n[k] = nil
		return nil
	} else {
		return errors.New("exitcode:1")
	}
}

func (n *fakeIPTables) NewChain(table, chain string) error {
	k := table + "/" + chain
	if _, ok := n.n[k]; ok {
		return fmt.Errorf("table/chain %s already exists", k)
	}
	n.n[k] = nil
	return nil
}

func (n *fakeIPTables) DeleteChain(table, chain string) error {
	k := table + "/" + chain
	if rules, ok := n.n[k]; ok {
		if len(rules) != 0 {
			return fmt.Errorf("table/chain %s is not empty", k)
		}
		delete(n.n, k)
		return nil
	} else {
		return fmt.Errorf("unknown table/chain %s", k)
	}
}

func NewFakeIPTablesRunner() NetfilterRunner {
	ipt4 := newFakeIPTables()
	v6Available := false
	var ipt6 iptablesInterface
	if use6, err := strconv.ParseBool(os.Getenv("TS_TEST_FAKE_NETFILTER_6")); use6 || err != nil {
		ipt6 = newFakeIPTables()
		v6Available = true
	}

	iptr := &iptablesRunner{ipt4, ipt6, v6Available, v6Available, v6Available}
	return iptr
}
