package collector

import (
	"crypto/x509"
	"encoding/pem"
	"os/exec"
	"path/filepath"
	"strings"
	"time"
)

type Certificate struct {
	Name       string `json:"name"`
	Subject    string `json:"subject"`
	Issuer     string `json:"issuer"`
	NotBefore  string `json:"not_before"`
	NotAfter   string `json:"not_after"`
	DaysLeft   int    `json:"days_left"`
	Serial     string `json:"serial"`
	SAN        string `json:"san,omitempty"`
	KeyUsage   string `json:"key_usage,omitempty"`
	IsCA       bool   `json:"is_ca"`
	Status     string `json:"status"` // "valid", "expiring", "expired"
	Source     string `json:"source"` // "opnsense", "file"
}

// CollectCertificates liest Zertifikate aus OPNsense config.xml und gaengigen Pfaden
func CollectCertificates() []Certificate {
	var certs []Certificate

	// 1. OPNsense config.xml Zertifikate
	certs = append(certs, collectOPNsenseCerts()...)

	// 2. Zertifikate aus /var/etc/cert/ (aktiv genutzte)
	certs = append(certs, collectCertDir("/var/etc/cert/")...)

	return certs
}

func collectOPNsenseCerts() []Certificate {
	// configctl: Zertifikate als Liste ausgeben
	// Alternativ: direkt config.xml parsen
	out, err := exec.Command("grep", "-A2", "<crt>", "/conf/config.xml").Output()
	if err != nil || len(out) == 0 {
		return collectFromConfigXML()
	}
	return collectFromConfigXML()
}

func collectFromConfigXML() []Certificate {
	// PHP-basiertes Auslesen der Zertifikate (OPNsense hat php verfuegbar)
	// Skript via stdin uebergeben, da -r bei langen Strings Probleme macht
	script := `<?php
require_once("config.inc");
$config = parse_config();
if (isset($config['cert'])) {
    foreach ($config['cert'] as $cert) {
        $x509 = openssl_x509_parse(base64_decode($cert['crt']));
        if ($x509 === false) continue;
        $name = isset($cert['descr']) ? $cert['descr'] : 'unnamed';
        $subject = isset($x509['subject']['CN']) ? $x509['subject']['CN'] : '';
        $issuer = isset($x509['issuer']['CN']) ? $x509['issuer']['CN'] : '';
        $notBefore = date('Y-m-d H:i:s', $x509['validFrom_time_t']);
        $notAfter = date('Y-m-d H:i:s', $x509['validTo_time_t']);
        $serial = $x509['serialNumberHex'];
        $san = '';
        if (isset($x509['extensions']['subjectAltName'])) {
            $san = $x509['extensions']['subjectAltName'];
        }
        $isCA = isset($x509['extensions']['basicConstraints']) &&
                strpos($x509['extensions']['basicConstraints'], 'CA:TRUE') !== false;
        echo "$name|$subject|$issuer|$notBefore|$notAfter|$serial|$san|" . ($isCA ? "1" : "0") . "\n";
    }
}
if (isset($config['ca'])) {
    foreach ($config['ca'] as $ca) {
        $x509 = openssl_x509_parse(base64_decode($ca['crt']));
        if ($x509 === false) continue;
        $name = isset($ca['descr']) ? $ca['descr'] : 'unnamed-ca';
        $subject = isset($x509['subject']['CN']) ? $x509['subject']['CN'] : '';
        $issuer = isset($x509['issuer']['CN']) ? $x509['issuer']['CN'] : '';
        $notBefore = date('Y-m-d H:i:s', $x509['validFrom_time_t']);
        $notAfter = date('Y-m-d H:i:s', $x509['validTo_time_t']);
        $serial = $x509['serialNumberHex'];
        echo "$name|$subject|$issuer|$notBefore|$notAfter|$serial||1\n";
    }
}
`

	cmd := exec.Command("/usr/local/bin/php")
	cmd.Stdin = strings.NewReader(script)
	out, err := cmd.Output()
	if err != nil {
		// Fallback: PEM-Dateien scannen
		return collectCertDir("/var/etc/cert/")
	}

	var certs []Certificate
	for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") {
		if line == "" {
			continue
		}
		fields := strings.SplitN(line, "|", 8)
		if len(fields) < 8 {
			continue
		}

		cert := Certificate{
			Name:      fields[0],
			Subject:   fields[1],
			Issuer:    fields[2],
			NotBefore: fields[3],
			NotAfter:  fields[4],
			Serial:    fields[5],
			SAN:       fields[6],
			IsCA:      fields[7] == "1",
			Source:    "opnsense",
		}

		// Status berechnen
		if notAfter, err := time.Parse("2006-01-02 15:04:05", cert.NotAfter); err == nil {
			daysLeft := int(time.Until(notAfter).Hours() / 24)
			cert.DaysLeft = daysLeft
			if daysLeft < 0 {
				cert.Status = "expired"
			} else if daysLeft < 30 {
				cert.Status = "expiring"
			} else {
				cert.Status = "valid"
			}
		}

		certs = append(certs, cert)
	}

	return certs
}

func collectCertDir(dir string) []Certificate {
	matches, err := filepath.Glob(filepath.Join(dir, "*.crt"))
	if err != nil {
		return nil
	}

	// Auch .pem Dateien
	pemMatches, _ := filepath.Glob(filepath.Join(dir, "*.pem"))
	matches = append(matches, pemMatches...)

	var certs []Certificate
	for _, path := range matches {
		out, err := exec.Command("cat", path).Output()
		if err != nil {
			continue
		}

		block, _ := pem.Decode(out)
		if block == nil || block.Type != "CERTIFICATE" {
			continue
		}

		x509Cert, err := x509.ParseCertificate(block.Bytes)
		if err != nil {
			continue
		}

		name := filepath.Base(path)
		name = strings.TrimSuffix(name, filepath.Ext(name))

		daysLeft := int(time.Until(x509Cert.NotAfter).Hours() / 24)
		status := "valid"
		if daysLeft < 0 {
			status = "expired"
		} else if daysLeft < 30 {
			status = "expiring"
		}

		var sans []string
		for _, dns := range x509Cert.DNSNames {
			sans = append(sans, "DNS:"+dns)
		}
		for _, ip := range x509Cert.IPAddresses {
			sans = append(sans, "IP:"+ip.String())
		}

		cert := Certificate{
			Name:      name,
			Subject:   x509Cert.Subject.CommonName,
			Issuer:    x509Cert.Issuer.CommonName,
			NotBefore: x509Cert.NotBefore.Format("2006-01-02 15:04:05"),
			NotAfter:  x509Cert.NotAfter.Format("2006-01-02 15:04:05"),
			DaysLeft:  daysLeft,
			Serial:    x509Cert.SerialNumber.Text(16),
			SAN:       strings.Join(sans, ", "),
			IsCA:      x509Cert.IsCA,
			Status:    status,
			Source:    "file",
		}

		certs = append(certs, cert)
	}

	return certs
}
