from __future__ import annotations
import datetime
import socket
import ssl
from typing import Iterable, List, Tuple

import requests

from ..model import Finding, ScanRequest

LEGACY_TLS_VERSIONS: Tuple[Tuple[str, ssl.TLSVersion], ...] = (
    ("TLS 1.0", ssl.TLSVersion.TLSv1),
    ("TLS 1.1", ssl.TLSVersion.TLSv1_1),
)


def evaluate_tls(request: ScanRequest) -> Iterable[Finding]:
    domain = request.domain
    context = ssl.create_default_context()
    findings: List[Finding] = []

    try:
        with socket.create_connection((domain, request.https_port), timeout=request.timeout) as sock:
            with context.wrap_socket(sock, server_hostname=domain) as tls_socket:
                cert = tls_socket.getpeercert()
    except ssl.SSLError as exc:
        findings.append(
            Finding(
                check="tls",
                title="TLS handshake failed",
                severity="high",
                description=f"Unable to negotiate TLS with {domain}:{request.https_port}: {exc}",
                remediation="Confirm that HTTPS is enabled and the certificate chain is valid.",
                impact="Clients cannot establish secure sessions with the service.",
                i18n_key="tls.handshake_failed",
                i18n_params={
                    "domain": domain,
                    "port": request.https_port,
                    "error": str(exc),
                },
            )
        )
        return findings
    except OSError as exc:
        findings.append(
            Finding(
                check="tls",
                title="HTTPS port unreachable",
                severity="medium",
                description=f"Network error reaching {domain}:{request.https_port}: {exc}",
                remediation="Ensure the server accepts connections on port 443 and no firewall blocks it.",
                impact="Visitors may be forced onto plaintext HTTP or experience downtime.",
                i18n_key="tls.https_port_unreachable",
                i18n_params={
                    "domain": domain,
                    "port": request.https_port,
                    "error": str(exc),
                },
            )
        )
        return findings

    if not cert:
        findings.append(
            Finding(
                check="tls",
                title="Certificate missing",
                severity="high",
                description="TLS negotiation succeeded but no certificate data was returned.",
                remediation="Reissue the TLS certificate and install it correctly on the load balancer.",
                impact="Browsers will block the connection because the certificate chain is incomplete.",
                i18n_key="tls.certificate_missing",
            )
        )
        return findings

    _check_cert_dates(cert, findings)
    _check_cert_hostname(cert, domain, findings)
    findings.extend(_check_legacy_tls(domain, request))
    findings.extend(_check_http_downgrade(domain, request))

    return findings


def _check_cert_dates(cert: dict, findings: List[Finding]) -> None:
    not_after = cert.get("notAfter")
    not_before = cert.get("notBefore")

    def _parse(value: str) -> datetime.datetime:
        return datetime.datetime.strptime(value, "%b %d %H:%M:%S %Y %Z")

    now = datetime.datetime.now(datetime.UTC)

    if not_before:
        nb = _parse(not_before)
        if nb > now:
            findings.append(
                Finding(
                    check="tls",
                    title="Certificate not yet valid",
                    severity="medium",
                    description=f"Certificate validity begins {nb.isoformat()}Z.",
                    remediation="Regenerate the certificate with the correct validity window.",
                    i18n_key="tls.certificate_not_yet_valid",
                    i18n_params={"date": f"{nb.isoformat()}Z"},
                )
            )

    if not_after:
        na = _parse(not_after)
        delta = na - now
        if delta.total_seconds() < 0:
            findings.append(
                Finding(
                    check="tls",
                    title="Certificate expired",
                    severity="high",
                    description=f"Certificate expired {na.isoformat()}Z.",
                    remediation="Renew the certificate immediately and deploy the new chain.",
                    i18n_key="tls.certificate_expired",
                    i18n_params={"date": f"{na.isoformat()}Z"},
                )
            )
        elif delta.days < 14:
            findings.append(
                Finding(
                    check="tls",
                    title="Certificate near expiry",
                    severity="medium",
                    description=f"Certificate expires {na.isoformat()}Z ({delta.days} days remaining).",
                    remediation="Schedule certificate renewal before the expiry date.",
                    i18n_key="tls.certificate_near_expiry",
                    i18n_params={
                        "date": f"{na.isoformat()}Z",
                        "days": delta.days,
                    },
                )
            )


def _check_cert_hostname(cert: dict, domain: str, findings: List[Finding]) -> None:
    alt_names = []
    for entry in cert.get("subjectAltName", []):
        if entry[0] == "DNS":
            alt_names.append(entry[1])

    if not alt_names:
        findings.append(
            Finding(
                check="tls",
                title="No subject alternative names",
                severity="medium",
                description="Certificate lacks Subject Alternative Name entries.",
                remediation="Issue a certificate that includes SAN fields with the covered domains.",
                i18n_key="tls.no_san",
            )
        )
        return

    if not any(_matches_hostname(domain, alt) for alt in alt_names):
        findings.append(
            Finding(
                check="tls",
                title="Hostname not covered by certificate",
                severity="high",
                description=f"{domain} not present in SAN list: {', '.join(alt_names[:5])}",
                remediation="Regenerate the certificate with a SAN covering the requested hostname.",
                i18n_key="tls.hostname_not_covered",
                i18n_params={
                    "domain": domain,
                    "san_list": ", ".join(alt_names[:5]),
                },
            )
        )


def _matches_hostname(domain: str, pattern: str) -> bool:
    if pattern.startswith("*."):
        suffix = pattern[1:]
        return domain.endswith(suffix) and domain.count(".") >= pattern.count(".")
    return domain == pattern


def _check_legacy_tls(domain: str, request: ScanRequest) -> List[Finding]:
    findings: List[Finding] = []
    for name, version in LEGACY_TLS_VERSIONS:
        try:
            if _supports_version(domain, request.https_port, version, request.timeout):
                findings.append(
                    Finding(
                        check="tls",
                        title=f"{name} still accepted",
                        severity="medium",
                        description=f"The server negotiates deprecated protocol version {name}.",
                        remediation="Disable TLS 1.0/1.1 on the load balancer or web server configuration.",
                        impact="Legacy TLS weakens transport encryption and may fail compliance checks (PCI DSS, NIST).",
                        i18n_key="tls.legacy_protocol",
                        i18n_params={"protocol": name},
                    )
                )
        except Exception:
            # If the handshake fails the version is effectively disabled.
            continue
    return findings


def _supports_version(domain: str, port: int, version: ssl.TLSVersion, timeout: float) -> bool:
    ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
    ctx.minimum_version = version
    ctx.maximum_version = version
    ctx.check_hostname = False
    ctx.verify_mode = ssl.CERT_NONE

    with socket.create_connection((domain, port), timeout=timeout) as sock:
        with ctx.wrap_socket(sock, server_hostname=domain) as tls_socket:
            tls_socket.do_handshake()
            return True


def _check_http_downgrade(domain: str, request: ScanRequest) -> List[Finding]:
    findings: List[Finding] = []
    url = f"http://{domain}:{request.http_port}"
    try:
        response = requests.get(
            url,
            headers={"User-Agent": request.user_agent},
            timeout=request.timeout,
            allow_redirects=True,
        )
    except requests.RequestException as exc:
        findings.append(
            Finding(
                check="tls",
                title="HTTP redirect check failed",
                severity="low",
                description=f"Unable to validate HTTP redirect behaviour: {exc}",
                remediation="Ensure port 80 is reachable or explicitly closed if HTTPS-only.",
                impact="Clients reaching HTTP may not be upgraded to HTTPS automatically.",
                i18n_key="tls.http_redirect_check_failed",
                i18n_params={"error": str(exc)},
            )
        )
        return findings

    if response.url.startswith("http://"):
        findings.append(
            Finding(
                check="tls",
                title="HTTPS redirect missing",
                severity="high",
                description=f"HTTP probe ended on plaintext endpoint {response.url}.",
                remediation="Redirect all HTTP traffic to HTTPS with 301/308 responses.",
                impact="Visitors risk man-in-the-middle interception when using HTTP.",
                i18n_key="tls.https_redirect_missing",
                i18n_params={"url": response.url},
            )
        )
    elif response.status_code not in (301, 302, 308) and response.history:
        first = response.history[0]
        if first.status_code not in (301, 302, 308):
            findings.append(
                Finding(
                    check="tls",
                    title="Non-permanent HTTP redirect",
                    severity="low",
                    description=f"Initial HTTP response returned {first.status_code} instead of permanent redirect.",
                    remediation="Respond with 301 or 308 to enforce permanent HTTPS upgrades.",
                    impact="SEO and caching layers may not cache HTTPS upgrade rules.",
                    i18n_key="tls.http_redirect_not_permanent",
                    i18n_params={"status": first.status_code},
                )
            )
    return findings
