"""
Work out why a site is unreachable, at the moment it fails.

Rule based, not guesswork: each probe answers one question, and the first that
fails names the cause. Running these at failure time matters, because by the
time somebody looks at the dashboard the symptom may have changed.
"""
import socket
import ssl
from datetime import datetime
from urllib.parse import urlparse

PROBE_TIMEOUT = 6


def _host_of(url):
    parsed = urlparse(url if url.startswith('http') else f'https://{url}')
    return parsed.hostname, (parsed.port or (443 if parsed.scheme == 'https' else 80))


def diagnose(url, error='', status_code=None):
    """
    Returns (cause, diagnosis, suggested_fix). Cause matches Incident.CAUSE_*.
    """
    from apps.ftlink.models import Incident

    host, port = _host_of(url)
    lines = []

    # HTTP answered, so DNS, TCP and TLS all worked. The fault is the app.
    if status_code:
        lines.append(f'HTTP {status_code} returned by {host}')
        if status_code >= 500:
            return (Incident.CAUSE_HTTP_5XX, '\n'.join(lines),
                    'Check the site service and its error log on the server.')
        if status_code in (401, 403):
            return (Incident.CAUSE_HTTP_4XX, '\n'.join(lines),
                    'Access is being refused. Check the firewall, Cloudflare rules or basic auth.')
        if status_code == 404:
            return (Incident.CAUSE_HTTP_4XX, '\n'.join(lines),
                    'Nothing served at the root. Check the document root and URL config.')
        return (Incident.CAUSE_HTTP_4XX, '\n'.join(lines), 'Check what the site returns at the root URL.')

    # 1. Does the name resolve?
    try:
        addresses = sorted({info[4][0] for info in socket.getaddrinfo(host, None)})
        lines.append(f'DNS resolves to {", ".join(addresses)}')
    except Exception as exc:
        return (Incident.CAUSE_DNS,
                f'DNS lookup for {host} failed: {exc}',
                'Check the A record and the nameservers for this domain.')

    # 2. Does the port accept a connection?
    try:
        with socket.create_connection((host, port), timeout=PROBE_TIMEOUT):
            lines.append(f'TCP connect to {host}:{port} succeeded')
    except socket.timeout:
        return (Incident.CAUSE_TIMEOUT, '\n'.join(lines + [f'TCP connect to port {port} timed out']),
                'Server is not answering. Check it is running and the firewall allows the port.')
    except Exception as exc:
        return (Incident.CAUSE_CONNECT, '\n'.join(lines + [f'TCP connect failed: {exc}']),
                'Connection refused. Check the web server is running on that port.')

    # 3. Is the certificate valid and in date?
    if port == 443:
        try:
            ctx = ssl.create_default_context()
            with socket.create_connection((host, port), timeout=PROBE_TIMEOUT) as sock:
                with ctx.wrap_socket(sock, server_hostname=host) as tls:
                    cert = tls.getpeercert()
            not_after = cert.get('notAfter')
            if not_after:
                expires = datetime.strptime(not_after, '%b %d %H:%M:%S %Y %Z')
                days = (expires - datetime.utcnow()).days
                lines.append(f'Certificate valid, expires in {days} day{"s" if days != 1 else ""}')
        except ssl.SSLCertVerificationError as exc:
            msg = str(exc)
            expired = 'expired' in msg.lower()
            return (Incident.CAUSE_TLS_EXPIRED if expired else Incident.CAUSE_TLS,
                    '\n'.join(lines + [f'TLS verification failed: {msg}']),
                    'Renew or reissue the certificate for this domain.')
        except Exception as exc:
            return (Incident.CAUSE_TLS, '\n'.join(lines + [f'TLS handshake failed: {exc}']),
                    'Check the certificate and the TLS configuration.')

    # Everything below HTTP looked fine, so the request itself is the problem.
    low = (error or '').lower()
    if 'timed out' in low or 'timeout' in low:
        return (Incident.CAUSE_TIMEOUT, '\n'.join(lines + [error]),
                'The site accepted the connection but never replied. Check for a hung worker or a slow query.')
    if 'redirect' in low:
        return (Incident.CAUSE_REDIRECT, '\n'.join(lines + [error]),
                'Redirect loop. Check the HTTPS and www rules.')
    return (Incident.CAUSE_UNKNOWN, '\n'.join(lines + [error or 'No further detail']),
            'Probes found nothing obviously wrong. Check the site by hand.')
