// Copyright 2025 Emiliano Spinella (eminwux)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// SPDX-License-Identifier: Apache-2.0

package cni

import (
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"path/filepath"

	libcni "github.com/containernetworking/cni/libcni"
	"github.com/eminwux/kukeon/internal/errdefs"
)

// bridgePluginType is the libcni "type" field value identifying the bridge
// plugin entry inside a conflist. Hoisted to a package constant so the same
// literal does not get tagged by goconst whenever a new conflist reader
// (ReadBridgeName, ReadSubnetCIDR, ...) is added.
const bridgePluginType = "bridge"

// LoadNetworkConfigList loads a CNI network config list from the given path.
func (m *Manager) LoadNetworkConfigList(configPath string) error {
	if configPath == "" {
		return errors.New("network config path is required")
	}

	conf, err := libcni.ConfListFromFile(configPath)
	if err != nil {
		return fmt.Errorf("failed to load CNI config list %s: %w", configPath, err)
	}

	m.netConf = conf
	return nil
}

// ExistsNetworkConfig checks if a network config exists and matches the expected network name.
func (m *Manager) ExistsNetworkConfig(networkName, configPath string) (bool, string, error) {
	if configPath == "" {
		configPath = filepath.Join(m.conf.CniConfigDir, networkName+".conflist")
	}

	data, err := os.ReadFile(configPath)
	if err != nil {
		if errors.Is(err, os.ErrNotExist) {
			return false, "", errdefs.ErrNetworkNotFound
		}
		return false, "", err
	}

	var raw map[string]interface{}
	if uErr := json.Unmarshal(data, &raw); uErr != nil {
		return false, "", uErr
	}

	if name, ok := raw["name"].(string); !ok || name != networkName {
		return false, "", errdefs.ErrNetworkNotFound
	}

	return true, configPath, nil
}

// ReadSubnetCIDR parses the conflist at configPath and returns the IPAM
// subnet of the first bridge plugin's first range. Returns
// errdefs.ErrNetworkNotFound when the file is missing,
// errdefs.ErrBridgePluginMissing when no bridge plugin is present, and a
// wrapped JSON error for malformed content. The returned string may be empty
// when the IPAM ranges entry has no `subnet` field, which callers should
// treat the same as a missing assignment.
func (m *Manager) ReadSubnetCIDR(configPath string) (string, error) {
	if configPath == "" {
		return "", errors.New("network config path is required")
	}

	data, err := os.ReadFile(configPath)
	if err != nil {
		if errors.Is(err, os.ErrNotExist) {
			return "", errdefs.ErrNetworkNotFound
		}
		return "", err
	}

	var raw map[string]interface{}
	if uErr := json.Unmarshal(data, &raw); uErr != nil {
		return "", fmt.Errorf("parse conflist: %w", uErr)
	}

	plugins, ok := raw["plugins"].([]interface{})
	if !ok {
		return "", errdefs.ErrBridgePluginMissing
	}

	for _, p := range plugins {
		plugin, pOK := p.(map[string]interface{})
		if !pOK {
			continue
		}
		if t, tOK := plugin["type"].(string); !tOK || t != bridgePluginType {
			continue
		}
		ipam, ipamOK := plugin["ipam"].(map[string]interface{})
		if !ipamOK {
			return "", nil
		}
		ranges, rOK := ipam["ranges"].([]interface{})
		if !rOK || len(ranges) == 0 {
			return "", nil
		}
		first, fOK := ranges[0].([]interface{})
		if !fOK || len(first) == 0 {
			return "", nil
		}
		entry, eOK := first[0].(map[string]interface{})
		if !eOK {
			return "", nil
		}
		subnet, _ := entry["subnet"].(string)
		return subnet, nil
	}

	return "", errdefs.ErrBridgePluginMissing
}

// ReadBridgeName parses the conflist at configPath and returns the `bridge` field of
// the first plugin whose `type` is "bridge". Returns errdefs.ErrNetworkNotFound if the
// file is missing, errdefs.ErrBridgePluginMissing if no bridge plugin is present, and
// a wrapped JSON error for malformed content. The returned string may be empty when
// the bridge plugin has no `bridge` field, which callers should treat as a mismatch.
func (m *Manager) ReadBridgeName(configPath string) (string, error) {
	if configPath == "" {
		return "", errors.New("network config path is required")
	}

	data, err := os.ReadFile(configPath)
	if err != nil {
		if errors.Is(err, os.ErrNotExist) {
			return "", errdefs.ErrNetworkNotFound
		}
		return "", err
	}

	var raw map[string]interface{}
	if uErr := json.Unmarshal(data, &raw); uErr != nil {
		return "", fmt.Errorf("parse conflist: %w", uErr)
	}

	plugins, ok := raw["plugins"].([]interface{})
	if !ok {
		return "", errdefs.ErrBridgePluginMissing
	}

	for _, p := range plugins {
		plugin, pOK := p.(map[string]interface{})
		if !pOK {
			continue
		}
		if t, tOK := plugin["type"].(string); !tOK || t != bridgePluginType {
			continue
		}
		bridge, _ := plugin["bridge"].(string)
		return bridge, nil
	}

	return "", errdefs.ErrBridgePluginMissing
}

// CreateNetworkWithConfig creates a CNI network conflist file using the provided NetworkConfig.
func (m *Manager) CreateNetworkWithConfig(cfg NetworkConfig) (string, error) {
	bridge := cfg.BridgeName
	if bridge == "" {
		bridge = defaultBridgeName
	}
	// Defense-in-depth: SafeBridgeName is supposed to keep the bridge under
	// IFNAMSIZ (it always emits k-{8hex} = 10 chars), but a caller that
	// bypasses it (or a future bug there) would otherwise write a conflist
	// that fails at bridge-create time with netlink ERANGE — a far less
	// diagnosable error than this one.
	if len(bridge) > maxBridgeNameLen {
		return "", fmt.Errorf(
			"%w: bridge %q is %d chars, max %d (IFNAMSIZ-1); call SafeBridgeName first",
			errdefs.ErrBridgeNameTooLong, bridge, len(bridge), maxBridgeNameLen,
		)
	}
	subnet := cfg.SubnetCIDR
	if subnet == "" {
		subnet = defaultSubnetCIDR
	}
	out, err := BuildDefaultConflist(cfg.Name, bridge, subnet)
	if err != nil {
		return "", err
	}
	target := filepath.Join(m.conf.CniConfigDir, cfg.Name+".conflist")
	if writeErr := os.WriteFile(target, out, 0o600); writeErr != nil {
		return "", writeErr
	}
	return target, nil
}

// CreateNetwork is a backward-compatible helper using discrete params.
func (m *Manager) CreateNetwork(networkName, bridgeName, subnetCIDR string) (string, error) {
	return m.CreateNetworkWithConfig(NetworkConfig{
		Name:       networkName,
		BridgeName: bridgeName,
		SubnetCIDR: subnetCIDR,
	})
}

// DeleteNetwork removes a CNI network config file from the filesystem.
func (m *Manager) DeleteNetwork(networkName, configPath string) error {
	if networkName == "" {
		return errors.New("network name is required")
	}

	if configPath == "" {
		configPath = filepath.Join(m.conf.CniConfigDir, networkName+".conflist")
	}

	// Check if file exists
	_, err := os.Stat(configPath)
	if err != nil {
		if errors.Is(err, os.ErrNotExist) {
			// Idempotent: file doesn't exist, consider it deleted
			return nil
		}
		return fmt.Errorf("failed to check network config file: %w", err)
	}

	// Verify the file contains the expected network name
	exists, actualPath, err := m.ExistsNetworkConfig(networkName, configPath)
	if err != nil && !errors.Is(err, errdefs.ErrNetworkNotFound) {
		return fmt.Errorf("failed to verify network config: %w", err)
	}
	if !exists {
		// Network config doesn't match or doesn't exist
		return nil
	}

	// Delete the config file
	if err = os.Remove(actualPath); err != nil {
		return fmt.Errorf("failed to delete network config file %s: %w", actualPath, err)
	}

	return nil
}
