package wgcontroller

import (
	"context"
	"fmt"
	"time"

	"github.com/h44z/wg-portal/internal/config"
	"github.com/h44z/wg-portal/internal/domain"
	"github.com/h44z/wg-portal/internal/lowlevel"
)

type MikrotikController struct {
	coreCfg *config.Config
	cfg     *config.BackendMikrotik

	client *lowlevel.MikrotikApiClient
}

func NewMikrotikController(coreCfg *config.Config, cfg *config.BackendMikrotik) (*MikrotikController, error) {
	client, err := lowlevel.NewMikrotikApiClient(coreCfg, cfg)
	if err != nil {
		return nil, fmt.Errorf("failed to create Mikrotik API client: %w", err)
	}

	return &MikrotikController{
		coreCfg: coreCfg,
		cfg:     cfg,

		client: client,
	}, nil
}

func (c MikrotikController) GetId() domain.InterfaceBackend {
	return domain.InterfaceBackend(c.cfg.Id)
}

// region wireguard-related

func (c MikrotikController) GetInterfaces(ctx context.Context) ([]domain.PhysicalInterface, error) {
	wgReply := c.client.Query(ctx, "/interface/wireguard", &lowlevel.MikrotikRequestOptions{
		PropList: []string{
			".id", "name", "public-key", "private-key", "listen-port", "mtu", "disabled", "running",
		},
	})
	if wgReply.Status != lowlevel.MikrotikApiStatusOk {
		return nil, fmt.Errorf("failed to query interfaces: %v", wgReply.Error)
	}

	interfaces := make([]domain.PhysicalInterface, 0, len(wgReply.Data))
	for _, wg := range wgReply.Data {
		physicalInterface, err := c.loadInterfaceData(ctx, wg)
		if err != nil {
			return nil, err
		}
		interfaces = append(interfaces, *physicalInterface)
	}

	return interfaces, nil
}

func (c MikrotikController) GetInterface(ctx context.Context, id domain.InterfaceIdentifier) (
	*domain.PhysicalInterface,
	error,
) {
	wgReply := c.client.Query(ctx, "/interface/wireguard", &lowlevel.MikrotikRequestOptions{
		PropList: []string{
			".id", "name", "public-key", "private-key", "listen-port", "mtu", "disabled", "running",
		},
		Filters: map[string]string{
			"name": string(id),
		},
	})
	if wgReply.Status != lowlevel.MikrotikApiStatusOk {
		return nil, fmt.Errorf("failed to query interface %s: %v", id, wgReply.Error)
	}

	if len(wgReply.Data) == 0 {
		return nil, fmt.Errorf("interface %s not found", id)
	}

	return c.loadInterfaceData(ctx, wgReply.Data[0])
}

func (c MikrotikController) loadInterfaceData(
	ctx context.Context,
	wireGuardObj lowlevel.GenericJsonObject,
) (*domain.PhysicalInterface, error) {
	deviceId := wireGuardObj.GetString(".id")
	deviceName := wireGuardObj.GetString("name")
	ifaceReply := c.client.Get(ctx, "/interface/"+deviceId, &lowlevel.MikrotikRequestOptions{
		PropList: []string{
			"name", "rx-byte", "tx-byte",
		},
	})
	if ifaceReply.Status != lowlevel.MikrotikApiStatusOk {
		return nil, fmt.Errorf("failed to query interface %s: %v", deviceId, ifaceReply.Error)
	}

	addrV4Reply := c.client.Query(ctx, "/ip/address", &lowlevel.MikrotikRequestOptions{
		PropList: []string{
			"address", "network",
		},
		Filters: map[string]string{
			"interface": deviceName,
			"dynamic":   "false", // we only want static addresses
			"disabled":  "false", // we only want addresses that are not disabled
		},
	})
	if addrV4Reply.Status != lowlevel.MikrotikApiStatusOk {
		return nil, fmt.Errorf("failed to query IPv4 addresses for interface %s: %v", deviceId, addrV4Reply.Error)
	}

	addrV6Reply := c.client.Query(ctx, "/ipv6/address", &lowlevel.MikrotikRequestOptions{
		PropList: []string{
			"address", "network",
		},
		Filters: map[string]string{
			"interface": deviceName,
			"dynamic":   "false", // we only want static addresses
			"disabled":  "false", // we only want addresses that are not disabled
		},
	})
	if addrV6Reply.Status != lowlevel.MikrotikApiStatusOk {
		return nil, fmt.Errorf("failed to query IPv6 addresses for interface %s: %v", deviceId, addrV6Reply.Error)
	}

	interfaceModel, err := c.convertWireGuardInterface(wireGuardObj, ifaceReply.Data, addrV4Reply.Data,
		addrV6Reply.Data)
	if err != nil {
		return nil, fmt.Errorf("interface convert failed for %s: %w", deviceName, err)
	}
	return &interfaceModel, nil
}

func (c MikrotikController) convertWireGuardInterface(
	wg, iface lowlevel.GenericJsonObject,
	ipv4, ipv6 []lowlevel.GenericJsonObject,
) (
	domain.PhysicalInterface,
	error,
) {
	// read data from wgctrl interface

	addresses := make([]domain.Cidr, 0, len(ipv4)+len(ipv6))
	for _, addr := range append(ipv4, ipv6...) {
		addrStr := addr.GetString("address")
		if addrStr == "" {
			continue
		}
		cidr, err := domain.CidrFromString(addrStr)
		if err != nil {
			continue
		}

		addresses = append(addresses, cidr)
	}

	pi := domain.PhysicalInterface{
		Identifier: domain.InterfaceIdentifier(wg.GetString("name")),
		KeyPair: domain.KeyPair{
			PrivateKey: wg.GetString("private-key"),
			PublicKey:  wg.GetString("public-key"),
		},
		ListenPort:    wg.GetInt("listen-port"),
		Addresses:     addresses,
		Mtu:           wg.GetInt("mtu"),
		FirewallMark:  0,
		DeviceUp:      wg.GetBool("running"),
		ImportSource:  "mikrotik",
		DeviceType:    "Mikrotik",
		BytesUpload:   uint64(iface.GetInt("tx-byte")),
		BytesDownload: uint64(iface.GetInt("rx-byte")),
	}

	return pi, nil
}

func (c MikrotikController) GetPeers(ctx context.Context, deviceId domain.InterfaceIdentifier) (
	[]domain.PhysicalPeer,
	error,
) {
	wgReply := c.client.Query(ctx, "/interface/wireguard/peers", &lowlevel.MikrotikRequestOptions{
		PropList: []string{
			".id", "name", "allowed-address", "client-address", "client-endpoint", "client-keepalive", "comment",
			"current-endpoint-address", "current-endpoint-port", "last-handshake", "persistent-keepalive",
			"public-key", "private-key", "preshared-key", "mtu", "disabled", "rx", "tx", "responder",
		},
		Filters: map[string]string{
			"interface": string(deviceId),
		},
	})
	if wgReply.Status != lowlevel.MikrotikApiStatusOk {
		return nil, fmt.Errorf("failed to query peers for %s: %v", deviceId, wgReply.Error)
	}

	if len(wgReply.Data) == 0 {
		return nil, nil
	}

	peers := make([]domain.PhysicalPeer, 0, len(wgReply.Data))
	for _, peer := range wgReply.Data {
		peerModel, err := c.convertWireGuardPeer(peer)
		if err != nil {
			return nil, fmt.Errorf("peer convert failed for %v: %w", peer.GetString("name"), err)
		}
		peers = append(peers, peerModel)
	}

	return peers, nil
}

func (c MikrotikController) convertWireGuardPeer(peer lowlevel.GenericJsonObject) (domain.PhysicalPeer, error) {
	keepAliveSeconds := 0
	duration, err := time.ParseDuration(peer.GetString("client-keepalive"))
	if err == nil {
		keepAliveSeconds = int(duration.Seconds())
	}

	currentEndpoint := ""
	if peer.GetString("current-endpoint-address") != "" && peer.GetString("current-endpoint-port") != "" {
		currentEndpoint = peer.GetString("current-endpoint-address") + ":" + peer.GetString("current-endpoint-port")
	}

	lastHandshakeTime := time.Time{}
	if peer.GetString("last-handshake") != "" {
		relDuration, err := time.ParseDuration(peer.GetString("last-handshake"))
		if err == nil {
			lastHandshakeTime = time.Now().Add(-relDuration)
		}
	}

	allowedAddresses, _ := domain.CidrsFromString(peer.GetString("allowed-address"))

	peerModel := domain.PhysicalPeer{
		Identifier: domain.PeerIdentifier(peer.GetString("public-key")),
		Endpoint:   currentEndpoint,
		AllowedIPs: allowedAddresses,
		KeyPair: domain.KeyPair{
			PublicKey:  peer.GetString("public-key"),
			PrivateKey: peer.GetString("private-key"),
		},
		PresharedKey:        domain.PreSharedKey(peer.GetString("preshared-key")),
		PersistentKeepalive: keepAliveSeconds,
		LastHandshake:       lastHandshakeTime,
		ProtocolVersion:     0, // Mikrotik does not support protocol versioning, so we set it to 0
		BytesUpload:         uint64(peer.GetInt("rx")),
		BytesDownload:       uint64(peer.GetInt("tx")),

		BackendExtras: make(map[string]interface{}),
	}

	peerModel.BackendExtras["MT-NAME"] = peer.GetString("name")
	peerModel.BackendExtras["MT-COMMENT"] = peer.GetString("comment")
	peerModel.BackendExtras["MT-RESPONDER"] = peer.GetString("responder")
	peerModel.BackendExtras["MT-ENDPOINT"] = peer.GetString("client-endpoint")
	peerModel.BackendExtras["MT-IP"] = peer.GetString("client-address")

	return peerModel, nil
}

func (c MikrotikController) SaveInterface(
	_ context.Context,
	id domain.InterfaceIdentifier,
	updateFunc func(pi *domain.PhysicalInterface) (*domain.PhysicalInterface, error),
) error {
	// TODO implement me
	return nil
}

func (c MikrotikController) DeleteInterface(_ context.Context, id domain.InterfaceIdentifier) error {
	// TODO implement me
	return nil
}

func (c MikrotikController) SavePeer(
	_ context.Context,
	deviceId domain.InterfaceIdentifier,
	id domain.PeerIdentifier,
	updateFunc func(pp *domain.PhysicalPeer) (*domain.PhysicalPeer, error),
) error {
	// TODO implement me
	return nil
}

func (c MikrotikController) DeletePeer(
	_ context.Context,
	deviceId domain.InterfaceIdentifier,
	id domain.PeerIdentifier,
) error {
	// TODO implement me
	return nil
}

// endregion wireguard-related

// region wg-quick-related

func (c MikrotikController) ExecuteInterfaceHook(id domain.InterfaceIdentifier, hookCmd string) error {
	// TODO implement me
	panic("implement me")
}

func (c MikrotikController) SetDNS(id domain.InterfaceIdentifier, dnsStr, dnsSearchStr string) error {
	// TODO implement me
	panic("implement me")
}

func (c MikrotikController) UnsetDNS(id domain.InterfaceIdentifier) error {
	// TODO implement me
	panic("implement me")
}

// endregion wg-quick-related

// region routing-related

func (c MikrotikController) SyncRouteRules(_ context.Context, rules []domain.RouteRule) error {
	// TODO implement me
	panic("implement me")
}

func (c MikrotikController) DeleteRouteRules(_ context.Context, rules []domain.RouteRule) error {
	// TODO implement me
	panic("implement me")
}

// endregion routing-related
