package nodeprocessor

import (
	"context"
	"crypto/tls"
	"crypto/x509"
	"fmt"
	"math"
	"net"
	"net/http"
	"strings"
	"time"

	"github.com/usual2970/certimate/internal/domain"
)

type inspectNode struct {
	node *domain.WorkflowNode
	*nodeProcessor
	*nodeOutputer
}

func NewInspectNode(node *domain.WorkflowNode) *inspectNode {
	return &inspectNode{
		node:          node,
		nodeProcessor: newNodeProcessor(node),
		nodeOutputer:  newNodeOutputer(),
	}
}

func (n *inspectNode) Process(ctx context.Context) error {
	n.logger.Info("entering inspect certificate node...")

	nodeConfig := n.node.GetConfigForInspect()

	err := n.inspect(ctx, nodeConfig)
	if err != nil {
		n.logger.Warn("inspect certificate failed: " + err.Error())
		return err
	}

	return nil
}

func (n *inspectNode) inspect(ctx context.Context, nodeConfig domain.WorkflowNodeConfigForInspect) error {
	maxRetries := 3
	retryInterval := 2 * time.Second

	var lastError error
	var certInfo *x509.Certificate

	host := nodeConfig.Host

	port := nodeConfig.Port
	if port == "" {
		port = "443"
	}

	domain := nodeConfig.Domain
	if domain == "" {
		domain = host
	}

	path := nodeConfig.Path
	if path != "" && !strings.HasPrefix(path, "/") {
		path = "/" + path
	}

	targetAddr := fmt.Sprintf("%s:%s", host, port)
	n.logger.Info(fmt.Sprintf("Inspecting certificate at %s (validating domain: %s)", targetAddr, domain))

	for attempt := 0; attempt < maxRetries; attempt++ {
		if attempt > 0 {
			n.logger.Info(fmt.Sprintf("Retry #%d connecting to %s", attempt, targetAddr))
			select {
			case <-ctx.Done():
				return ctx.Err()
			case <-time.After(retryInterval):
				// Wait for retry interval
			}
		}

		transport := &http.Transport{
			DialContext: (&net.Dialer{
				Timeout: 10 * time.Second,
			}).DialContext,
			TLSClientConfig: &tls.Config{
				InsecureSkipVerify: true,
				ServerName:         domain, // Set SNI to domain for proper certificate selection
			},
			ForceAttemptHTTP2: false,
			DisableKeepAlives: true,
		}

		client := &http.Client{
			Transport: transport,
			Timeout:   15 * time.Second,
			CheckRedirect: func(req *http.Request, via []*http.Request) error {
				return http.ErrUseLastResponse
			},
		}

		scheme := "https"
		urlStr := fmt.Sprintf("%s://%s", scheme, targetAddr)
		if path != "" {
			urlStr = urlStr + path
		}

		req, err := http.NewRequestWithContext(ctx, "HEAD", urlStr, nil)
		if err != nil {
			lastError = fmt.Errorf("failed to create HTTP request: %w", err)
			n.logger.Warn(fmt.Sprintf("Request creation attempt #%d failed: %s", attempt+1, lastError.Error()))
			continue
		}

		if domain != host {
			req.Host = domain
		}

		req.Header.Set("User-Agent", "CertificateValidator/1.0")
		req.Header.Set("Accept", "*/*")

		resp, err := client.Do(req)
		if err != nil {
			lastError = fmt.Errorf("HTTP request failed: %w", err)
			n.logger.Warn(fmt.Sprintf("Connection attempt #%d failed: %s", attempt+1, lastError.Error()))
			continue
		}

		if resp.TLS == nil || len(resp.TLS.PeerCertificates) == 0 {
			resp.Body.Close()
			lastError = fmt.Errorf("no TLS certificates received in HTTP response")
			n.logger.Warn(fmt.Sprintf("Certificate retrieval attempt #%d failed: %s", attempt+1, lastError.Error()))
			continue
		}

		certInfo = resp.TLS.PeerCertificates[0]
		resp.Body.Close()

		lastError = nil
		n.logger.Info(fmt.Sprintf("Successfully retrieved certificate from %s", targetAddr))
		break
	}

	if lastError != nil {
		return fmt.Errorf("failed to retrieve certificate after %d attempts: %w", maxRetries, lastError)
	}

	if certInfo == nil {
		outputs := map[string]any{
			outputCertificateValidatedKey: "false",
			outputCertificateDaysLeftKey:  "0",
		}
		n.setOutputs(outputs)
		return nil
	}

	now := time.Now()

	isValidTime := now.Before(certInfo.NotAfter) && now.After(certInfo.NotBefore)

	domainMatch := true
	if err := certInfo.VerifyHostname(domain); err != nil {
		domainMatch = false
	}

	isValid := isValidTime && domainMatch

	daysRemaining := math.Floor(certInfo.NotAfter.Sub(now).Hours() / 24)

	isValidStr := "false"
	if isValid {
		isValidStr = "true"
	}

	outputs := map[string]any{
		outputCertificateValidatedKey: isValidStr,
		outputCertificateDaysLeftKey:  fmt.Sprintf("%d", int(daysRemaining)),
	}

	n.setOutputs(outputs)

	n.logger.Info(fmt.Sprintf("Certificate inspection completed - Target: %s, Domain: %s, Valid: %s, Days Remaining: %d",
		targetAddr, domain, isValidStr, int(daysRemaining)))

	return nil
}

func (n *inspectNode) setOutputs(outputs map[string]any) {
	n.outputs = outputs
}