#!/usr/bin/python

from __future__ import absolute_import, division, print_function
__metaclass__ = type

import os
import subprocess
import time
from datetime import datetime

class CertUtils:
    _domain_cert_mapping = None
    _cert_snapshot = None

    @staticmethod
    def run_openssl(cert_path):
        try:
            output = subprocess.check_output(
                ['openssl', 'x509', '-in', cert_path, '-noout', '-text'],
                universal_newlines=True
            )
            return output
        except subprocess.CalledProcessError:
            return ""

    @staticmethod
    def run_openssl_dates(cert_path):
        """
        Returns (not_before_ts, not_after_ts) as POSIX timestamps or (None, None) on failure.
        """
        try:
            output = subprocess.check_output(
                ['openssl', 'x509', '-in', cert_path, '-noout', '-startdate', '-enddate'],
                universal_newlines=True
            )
            nb, na = None, None
            for line in output.splitlines():
                line = line.strip()
                if line.startswith('notBefore='):
                    nb = line.split('=', 1)[1].strip()
                elif line.startswith('notAfter='):
                    na = line.split('=', 1)[1].strip()
            def _parse(openssl_dt):
                # OpenSSL format example: "Oct 10 12:34:56 2025 GMT"
                return int(datetime.strptime(openssl_dt, "%b %d %H:%M:%S %Y %Z").timestamp())
            return (_parse(nb) if nb else None, _parse(na) if na else None)
        except Exception:
            return (None, None)

    @staticmethod
    def extract_sans(cert_text):
        dns_entries = []
        in_san = False
        for line in cert_text.splitlines():
            line = line.strip()
            if 'X509v3 Subject Alternative Name:' in line:
                in_san = True
                continue
            if in_san:
                if not line:
                    break
                dns_entries += [e.strip().replace('DNS:', '') for e in line.split(',') if e.strip()]
        return dns_entries

    @staticmethod
    def list_cert_files(cert_base_path):
        cert_files = []
        for root, dirs, files in os.walk(cert_base_path):
            if 'cert.pem' in files:
                cert_files.append(os.path.join(root, 'cert.pem'))
        return cert_files

    @staticmethod
    def matches(domain, san):
        """RFC compliant SAN matching."""
        if san.startswith('*.'):
            base = san[2:]
            # Wildcard matches ONLY one additional label
            if domain == base:
                return False
            if domain.endswith('.' + base) and domain.count('.') == base.count('.') + 1:
                return True
            return False
        else:
            return domain == san

    @classmethod
    def build_snapshot(cls, cert_base_path):
        snapshot = []
        for cert_file in cls.list_cert_files(cert_base_path):
            try:
                stat = os.stat(cert_file)
                snapshot.append((cert_file, stat.st_mtime, stat.st_size))
            except FileNotFoundError:
                continue
        snapshot.sort()
        return snapshot

    @classmethod
    def snapshot_changed(cls, cert_base_path):
        current_snapshot = cls.build_snapshot(cert_base_path)
        if cls._cert_snapshot != current_snapshot:
            cls._cert_snapshot = current_snapshot
            return True
        return False

    @classmethod
    def refresh_cert_mapping(cls, cert_base_path, debug=False):
        """
        Build mapping: SAN -> list of entries
        entry = {
            'folder': str,
            'cert_path': str,
            'mtime': float,
            'not_before': int|None,
            'not_after': int|None,
            'is_wildcard': bool
        }
        """
        cert_files = cls.list_cert_files(cert_base_path)
        mapping = {}
        for cert_path in cert_files:
            cert_text = cls.run_openssl(cert_path)
            if not cert_text:
                continue
            sans = cls.extract_sans(cert_text)
            folder = os.path.basename(os.path.dirname(cert_path))
            try:
                mtime = os.stat(cert_path).st_mtime
            except FileNotFoundError:
                mtime = 0.0
            nb, na = cls.run_openssl_dates(cert_path)

            for san in sans:
                entry = {
                    'folder': folder,
                    'cert_path': cert_path,
                    'mtime': mtime,
                    'not_before': nb,
                    'not_after': na,
                    'is_wildcard': san.startswith('*.'),
                }
                mapping.setdefault(san, []).append(entry)

        cls._domain_cert_mapping = mapping
        if debug:
            print(f"[DEBUG] Refreshed domain-to-cert mapping (counts): "
                  f"{ {k: len(v) for k, v in mapping.items()} }")

    @classmethod
    def ensure_cert_mapping(cls, cert_base_path, debug=False):
        if cls._domain_cert_mapping is None or cls.snapshot_changed(cert_base_path):
            cls.refresh_cert_mapping(cert_base_path, debug)

    @staticmethod
    def _score_entry(entry):
        """
        Return tuple used for sorting newest-first:
        (not_before or -inf, mtime)
        """
        nb = entry.get('not_before')
        mtime = entry.get('mtime', 0.0)
        return (nb if nb is not None else -1, mtime)

    @classmethod
    def find_cert_for_domain(cls, domain, cert_base_path, debug=False):
        cls.ensure_cert_mapping(cert_base_path, debug)

        candidates_exact = []
        candidates_wild = []

        for san, entries in cls._domain_cert_mapping.items():
            if san == domain:
                candidates_exact.extend(entries)
            elif san.startswith('*.'):
                base = san[2:]
                if domain.count('.') == base.count('.') + 1 and domain.endswith('.' + base):
                    candidates_wild.extend(entries)

        def _pick_newest(entries):
            if not entries:
                return None
            # newest by (not_before, mtime)
            best = max(entries, key=cls._score_entry)
            return best

        best_exact = _pick_newest(candidates_exact)
        best_wild = _pick_newest(candidates_wild)

        if best_exact and debug:
            print(f"[DEBUG] Best exact match for {domain}: {best_exact['folder']} "
                  f"(not_before={best_exact['not_before']}, mtime={best_exact['mtime']})")
        if best_wild and debug:
            print(f"[DEBUG] Best wildcard match for {domain}: {best_wild['folder']} "
                  f"(not_before={best_wild['not_before']}, mtime={best_wild['mtime']})")

        # Prefer exact if it exists; otherwise wildcard
        chosen = best_exact or best_wild

        if chosen:
            return chosen['folder']

        if debug:
            print(f"[DEBUG] No certificate folder found for {domain}")

        return None
