package client

import (
	"bytes"
	"context"
	"encoding/binary"
	"encoding/json"
	"fmt"
	"log"
	"os"
	"os/exec"
	"path/filepath"
	"slices"
	"strings"
	"syscall"

	"github.com/mdlayher/genetlink"
	"github.com/mdlayher/netlink"
)

// Constants for the DEEPIN_ENOTIFY family
const (
	FamilyName     = "DEEPIN_ENOTIFY"
	MulticastGroup = "err_events" // Fixed: matches kernel definition
)

// OptVerbose is a global option for verbose mode
var OptVerbose = false

// Command types
const (
	CmdErrRoFs   = 1 // DEEPIN_ERR_NOTIFY_CMD_ERR_ROFS
	CmdEnable    = 2 // DEEPIN_ERR_NOTIFY_CMD_ENABLE
	CmdDisable   = 3 // DEEPIN_ERR_NOTIFY_CMD_DISABLE
	CmdGetStatus = 4 // DEEPIN_ERR_NOTIFY_CMD_GET_STATUS
)

// Attribute types
const (
	AttrFilename    = 1 // DEEPIN_ERR_NOTIFY_ATTR_FILENAME - Filename (string)
	AttrInode       = 2 // DEEPIN_ERR_NOTIFY_ATTR_INODE - Inode (u64)
	AttrInodeParent = 3 // DEEPIN_ERR_NOTIFY_ATTR_INODE_PARENT - Bool: inode from parent (u8)
	AttrPID         = 4 // DEEPIN_ERR_NOTIFY_ATTR_PID - Process ID (u32)
	AttrPPID        = 5 // DEEPIN_ERR_NOTIFY_ATTR_PPID - Parent Process ID (u32)
	AttrComm        = 6 // DEEPIN_ERR_NOTIFY_ATTR_COMM - Process Short Name (string, 15 chars max)
	AttrUID         = 7 // DEEPIN_ERR_NOTIFY_ATTR_UID - User ID (u32)
	AttrGID         = 8 // DEEPIN_ERR_NOTIFY_ATTR_GID - Group ID (u32)
	AttrStatus      = 9 // DEEPIN_ERR_NOTIFY_ATTR_STATUS - Enable/Disable Status (u8)
)

// NotificationMessage represents a parsed notification message
type NotificationMessage struct {
	Filenames    []string // Support multiple filenames from kernel
	Inodes       []uint64 // Support multiple inodes from kernel
	InodeParents []bool   // Flags indicating if inode is from parent dentry
	PID          uint32
	PPID         uint32
	UID          uint32
	GID          uint32
	Comm         string // Process short name (15 chars max)
}

// ValidateInodes verifies that the inodes match the actual files on disk.
// Returns true if validation passes, false if there's a mismatch.
// If validation fails, the message should be ignored.
// When InodeParents[i] is true, the inode is from the parent directory.
//
// Validation rules:
// 1. Filenames, Inodes, and InodeParents arrays must have the same length (invalid message otherwise)
// 2. At least one inode must validate successfully for the message to be accepted
//
// Validation strategy: "First match succeeds" (design feature)
//   - For multi-file notifications, only the first successfully validated
//     filename/inode pair is checked; remaining pairs are NOT validated
//   - This is intentional: once any file/inode validates, the notification
//     is considered legitimate and processing continues
//   - Rationale: In many cases, kernel reports multiple paths to the same
//     operation (e.g., rename), validating one is sufficient
func (n *NotificationMessage) ValidateInodes() bool {
	// Check if arrays have the same length (validation prerequisite)
	if len(n.Filenames) != len(n.Inodes) || len(n.Filenames) != len(n.InodeParents) {
		log.Printf("Error: Invalid message - array length mismatch (filenames=%d, inodes=%d, inode_parents=%d)",
			len(n.Filenames), len(n.Inodes), len(n.InodeParents))
		return false
	}

	// If no inodes provided, failed validation
	if len(n.Inodes) == 0 {
		log.Printf("Error: No inodes provided, failed validation")
		return false
	}

	// Validate inodes in order, any one success means validation passes
	// Try each inode until one succeeds or all fail
	for i := 0; i < len(n.Filenames); i++ {
		filename := filepath.Clean(n.Filenames[i])
		inode := n.Inodes[i]
		isParentInode := n.InodeParents[i]
		if inode == 0 {
			log.Printf("Warning: Inode is 0, skipping validation")
			continue
		}

		if isParentInode {
			// Inode is from parent directory, validate parent's inode
			// Extract parent directory path
			parentDir := filename
			if idx := strings.LastIndex(filename, "/"); idx > 0 {
				parentDir = filename[:idx]
			} else if idx == 0 {
				parentDir = "/"
			}

			// Get parent directory info
			parentInfo, err := os.Lstat(parentDir)
			if err != nil {
				log.Printf("Warning: Cannot stat parent directory %s: %v (trying next)", parentDir, err)
				continue
			}

			// Get the inode from parent directory
			parentStat, ok := parentInfo.Sys().(*syscall.Stat_t)
			if !ok {
				log.Printf("Warning: Cannot get stat info for parent directory %s (trying next)", parentDir)
				continue
			}

			// Compare parent directory inode
			if parentStat.Ino != inode {
				log.Printf("Error: Parent inode mismatch for file %s: expected %d, got %d",
					filename, inode, parentStat.Ino)
				return false
			}

			// Success! Validation passed
			// First match succeeds - remaining files are NOT validated (by design)
			log.Printf("Info: Validated parent inode for %s (parent: %s, inode: %d)",
				filename, parentDir, inode)
			return true
		}

		// Normal case: validate the file's own inode
		fileInfo, err := os.Lstat(filename)
		if err != nil {
			// File doesn't exist or can't be accessed - might have been deleted
			log.Printf("Warning: Cannot stat file %s: %v (trying next)", filename, err)
			continue
		}

		// Get the inode from file info
		stat, ok := fileInfo.Sys().(*syscall.Stat_t)
		if !ok {
			log.Printf("Warning: Cannot get stat info for file %s (trying next)", filename)
			continue
		}

		// Compare inode numbers
		if stat.Ino != inode {
			log.Printf("Error: Inode mismatch for file %s: expected %d, got %d",
				filename, inode, stat.Ino)
			return false
		}

		// Success! Validation passed
		// First match succeeds - remaining files are NOT validated (by design)
		log.Printf("Info: Validated inode for %s (inode: %d)", filename, inode)
		return true
	}

	// All inodes failed validation
	log.Printf("Error: All %d inode(s) failed validation", len(n.Inodes))
	return false
}

// Client wraps the genetlink connection and provides high-level operations
type Client struct {
	conn    *genetlink.Conn
	family  genetlink.Family
	groupID uint32
}

// NewClient creates a new EROFS notification client
// This function initializes the client but does NOT automatically enable
// error notification or join multicast groups.
// Use NewClientForMonitoring() for a ready-to-use monitoring client.
func NewClient() (*Client, error) {
	conn, err := genetlink.Dial(nil)
	if err != nil {
		return nil, fmt.Errorf("failed to dial genetlink: %w", err)
	}

	client := &Client{conn: conn}

	// Get family information
	family, err := conn.GetFamily(FamilyName)
	if err != nil {
		conn.Close()
		return nil, fmt.Errorf("failed to resolve family '%s': %w", FamilyName, err)
	}
	client.family = family

	// Find multicast group ID (but don't join yet)
	var groupID uint32
	for _, g := range family.Groups {
		if g.Name == MulticastGroup {
			groupID = g.ID
			break
		}
	}
	if groupID == 0 {
		conn.Close()
		return nil, fmt.Errorf("multicast group '%s' not found", MulticastGroup)
	}
	client.groupID = groupID

	return client, nil
}

// NewClientForMonitoring creates a client ready for monitoring notifications
// It checks prerequisites, enables error notification via netlink, and joins multicast group
func NewClientForMonitoring() (*Client, error) {
	// Check mount options prerequisites
	options, err := getUsrMntOptions()
	if err != nil {
		return nil, fmt.Errorf("failed to get usr mount options: %w", err)
	}
	if !slices.Contains(options, "ro") {
		return nil, fmt.Errorf("usr mount options do not contain 'ro'")
	}

	// Create client
	client, err := NewClient()
	if err != nil {
		return nil, err
	}

	// Enable error notification via netlink
	if err := client.Enable(); err != nil {
		client.Close()
		return nil, fmt.Errorf("failed to enable error notification: %w", err)
	}

	// Verify it's enabled
	enabled, err := client.GetStatus()
	if err != nil {
		client.Close()
		return nil, fmt.Errorf("failed to check error notification status: %w", err)
	}
	if !enabled {
		client.Close()
		return nil, fmt.Errorf("error notification is not enabled after enable command")
	}

	// Join multicast group for receiving notifications
	if err := client.conn.JoinGroup(client.groupID); err != nil {
		client.Close()
		return nil, fmt.Errorf("failed to join multicast group: %w", err)
	}

	return client, nil
}

// Close closes the client connection
func (c *Client) Close() error {
	if c.conn != nil {
		if c.groupID != 0 {
			c.conn.LeaveGroup(c.groupID)
		}
		return c.conn.Close()
	}
	return nil
}

// Enable sends netlink command to enable error notification
func (c *Client) Enable() error {
	// Construct genetlink message with ENABLE command
	req := genetlink.Message{
		Header: genetlink.Header{
			Command: CmdEnable,
			Version: c.family.Version,
		},
	}

	// Send request with ACK flag to get error feedback from netlink layer
	_, err := c.conn.Send(req, c.family.ID, netlink.Request|netlink.Acknowledge)
	if err != nil {
		return fmt.Errorf("failed to send enable command: %w", err)
	}

	// Receive ACK or error from netlink layer
	// If the command fails (e.g., permission denied), netlink will return an error
	_, netlinkMsgs, err := c.conn.Receive()
	if err != nil {
		// Check if it's a permission error
		errStr := err.Error()
		if strings.Contains(errStr, "operation not permitted") ||
			strings.Contains(errStr, "permission denied") ||
			strings.Contains(errStr, "EPERM") {
			return fmt.Errorf("insufficient permissions (need CAP_NET_ADMIN or root): %w", err)
		}
		return fmt.Errorf("failed to receive ACK for enable command: %w", err)
	}

	// Check netlink messages for error codes
	// If there's an error message, it will be in the netlink messages
	for _, nlMsg := range netlinkMsgs {
		// Check if this is an error message (type NLMSG_ERROR = 2)
		if nlMsg.Header.Type == 2 && len(nlMsg.Data) >= 4 {
			// Parse error code from the message (native-endian int32)
			// Netlink uses native byte order, and errno is typically negative
			var errnoRaw int32
			err := binary.Read(bytes.NewReader(nlMsg.Data[:4]), binary.NativeEndian, &errnoRaw)
			if err != nil {
				return fmt.Errorf("failed to parse errno: %w", err)
			}
			// errno is typically negative, take absolute value
			errno := syscall.Errno(-errnoRaw)
			if errnoRaw != 0 {
				// Non-zero error code means the command failed
				if errno == syscall.EPERM {
					return fmt.Errorf("insufficient permissions (need CAP_NET_ADMIN or root)")
				}
				return fmt.Errorf("enable command failed with errno: %v (%d)", errno, errnoRaw)
			}
		}
	}

	return nil
}

// Disable sends netlink command to disable error notification
func (c *Client) Disable() error {
	// Construct genetlink message with DISABLE command
	req := genetlink.Message{
		Header: genetlink.Header{
			Command: CmdDisable,
			Version: c.family.Version,
		},
	}

	// Send request with ACK flag to get error feedback from netlink layer
	_, err := c.conn.Send(req, c.family.ID, netlink.Request|netlink.Acknowledge)
	if err != nil {
		return fmt.Errorf("failed to send disable command: %w", err)
	}

	// Receive ACK or error from netlink layer
	// If the command fails (e.g., permission denied), netlink will return an error
	_, netlinkMsgs, err := c.conn.Receive()
	if err != nil {
		// Check if it's a permission error
		errStr := err.Error()
		if strings.Contains(errStr, "operation not permitted") ||
			strings.Contains(errStr, "permission denied") ||
			strings.Contains(errStr, "EPERM") {
			return fmt.Errorf("insufficient permissions (need CAP_NET_ADMIN or root): %w", err)
		}
		return fmt.Errorf("failed to receive ACK for disable command: %w", err)
	}

	// Check netlink messages for error codes
	// If there's an error message, it will be in the netlink messages
	for _, nlMsg := range netlinkMsgs {
		// Check if this is an error message (type NLMSG_ERROR = 2)
		if nlMsg.Header.Type == 2 && len(nlMsg.Data) >= 4 {
			// Parse error code from the message (native-endian int32)
			// Netlink uses native byte order, and errno is typically negative
			var errnoRaw int32
			err := binary.Read(bytes.NewReader(nlMsg.Data[:4]), binary.NativeEndian, &errnoRaw)
			if err != nil {
				return fmt.Errorf("failed to parse errno: %w", err)
			}
			// errno is typically negative, take absolute value
			errno := syscall.Errno(-errnoRaw)
			if errnoRaw != 0 {
				// Non-zero error code means the command failed
				if errno == syscall.EPERM {
					return fmt.Errorf("insufficient permissions (need CAP_NET_ADMIN or root)")
				}
				return fmt.Errorf("disable command failed with errno: %v (%d)", errno, errnoRaw)
			}
		}
	}

	return nil
}

// GetStatus sends netlink command to query error notification status
// Returns true if enabled, false if disabled
func (c *Client) GetStatus() (bool, error) {
	// Construct genetlink message with GET_STATUS command
	req := genetlink.Message{
		Header: genetlink.Header{
			Command: CmdGetStatus,
			Version: c.family.Version,
		},
	}

	// Send request and receive response
	msgs, err := c.conn.Execute(req, c.family.ID, netlink.Request)
	if err != nil {
		return false, fmt.Errorf("failed to send get_status command: %w", err)
	}

	if len(msgs) == 0 {
		return false, fmt.Errorf("no response received for get_status command")
	}

	// Parse the response
	ad, err := netlink.NewAttributeDecoder(msgs[0].Data)
	if err != nil {
		return false, fmt.Errorf("failed to create attribute decoder: %w", err)
	}

	var status uint8
	for ad.Next() {
		if ad.Type() == AttrStatus {
			status = ad.Uint8()
			break
		}
	}

	if err := ad.Err(); err != nil {
		return false, fmt.Errorf("error decoding attributes: %w", err)
	}

	return status != 0, nil
}

// ReceiveMessages receives and parses notification messages
func (c *Client) ReceiveMessages(ctx context.Context) (<-chan NotificationMessage, <-chan error) {
	msgCh := make(chan NotificationMessage, 10)
	errCh := make(chan error, 1)

	go func() {
		defer close(msgCh)
		defer close(errCh)

		for {
			select {
			case <-ctx.Done():
				return
			default:
			}

			msgs, _, err := c.conn.Receive()
			if err != nil {
				select {
				case <-ctx.Done():
					return
				case errCh <- fmt.Errorf("error receiving message: %w", err):
				default:
				}
				continue
			}

			for _, msg := range msgs {
				// Skip done messages - genetlink messages don't have the same header structure
				// We'll process all messages that come through

				notification, err := c.parseMessage(msg)
				if err != nil {
					log.Printf("Failed to parse message: %v", err)
					continue
				}

				if OptVerbose {
					fmt.Printf("Notification: %+v\n", notification)
				}

				if !notification.ValidateInodes() {
					// Inode validation failed, skip this message
					log.Printf("Inode validation failed, skipping message")
					continue
				}

				select {
				case <-ctx.Done():
					return
				case msgCh <- *notification:
				default:
					// Channel is full, skip this message
				}
			}
		}
	}()

	return msgCh, errCh
}

// parseMessage parses a genetlink message into a NotificationMessage
func (c *Client) parseMessage(msg genetlink.Message) (*NotificationMessage, error) {
	if msg.Header.Command != CmdErrRoFs {
		return nil, fmt.Errorf("unexpected command: %d", msg.Header.Command)
	}

	notification := &NotificationMessage{}

	ad, err := netlink.NewAttributeDecoder(msg.Data)
	if err != nil {
		return nil, fmt.Errorf("failed to create attribute decoder: %w", err)
	}

	for ad.Next() {
		switch ad.Type() {
		case AttrFilename:
			// Append filename instead of overwriting - supports multiple filenames
			notification.Filenames = append(notification.Filenames, ad.String())
		case AttrInode:
			// Append inode instead of overwriting - supports multiple inodes
			notification.Inodes = append(notification.Inodes, ad.Uint64())
		case AttrInodeParent:
			// Append inode parent flag - indicates if inode is from parent dentry
			notification.InodeParents = append(notification.InodeParents, ad.Uint8() != 0)
		case AttrPID:
			notification.PID = ad.Uint32()
		case AttrPPID:
			notification.PPID = ad.Uint32()
		case AttrUID:
			notification.UID = ad.Uint32()
		case AttrGID:
			notification.GID = ad.Uint32()
		case AttrComm:
			notification.Comm = ad.String()
		}
	}

	if err := ad.Err(); err != nil {
		return nil, fmt.Errorf("error decoding attributes: %w", err)
	}

	return notification, nil
}

// GetFamilyInfo returns information about the netlink family
func (c *Client) GetFamilyInfo() (string, uint16, uint32) {
	return c.family.Name, c.family.ID, c.groupID
}

// FindmntEntry represents each filesystem entry of findmnt output
type FindmntEntry struct {
	Target  string `json:"target"`
	Source  string `json:"source"`
	Fstype  string `json:"fstype"`
	Options string `json:"options"`
}

// FindmntOutput mirrors the structure of findmnt's JSON output for 'findmnt /usr -J'
type FindmntOutput struct {
	Filesystems []FindmntEntry `json:"filesystems"`
}

// getUsrMntOptions executes 'findmnt /usr -J', parses the JSON, and returns mount options as []string
func getUsrMntOptions() ([]string, error) {
	cmd := exec.Command("findmnt", "/usr", "-J")
	out, err := cmd.Output()
	if err != nil {
		return nil, fmt.Errorf("failed to run findmnt: %w", err)
	}

	var parsed FindmntOutput
	if err := json.Unmarshal(out, &parsed); err != nil {
		return nil, fmt.Errorf("failed to unmarshal findmnt json: %w", err)
	}

	if len(parsed.Filesystems) == 0 {
		return nil, fmt.Errorf("no filesystems found in findmnt output")
	}

	optionsRaw := parsed.Filesystems[0].Options
	optionsSplit := strings.Split(optionsRaw, ",")
	return optionsSplit, nil
}
