from django.conf import settings
from django.core.exceptions import ImproperlyConfigured

from .. import Error, Tags, Warning, register

CROSS_ORIGIN_OPENER_POLICY_VALUES = {
    "same-origin",
    "same-origin-allow-popups",
    "unsafe-none",
}
REFERRER_POLICY_VALUES = {
    "no-referrer",
    "no-referrer-when-downgrade",
    "origin",
    "origin-when-cross-origin",
    "same-origin",
    "strict-origin",
    "strict-origin-when-cross-origin",
    "unsafe-url",
}

SECRET_KEY_INSECURE_PREFIX = "django-insecure-"
SECRET_KEY_MIN_LENGTH = 50
SECRET_KEY_MIN_UNIQUE_CHARACTERS = 5

SECRET_KEY_WARNING_MSG = (
    f"Your %s has less than {SECRET_KEY_MIN_LENGTH} characters, less than "
    f"{SECRET_KEY_MIN_UNIQUE_CHARACTERS} unique characters, or it's prefixed "
    f"with '{SECRET_KEY_INSECURE_PREFIX}' indicating that it was generated "
    f"automatically by Django. Please generate a long and random value, "
    f"otherwise many of Django's security-critical features will be "
    f"vulnerable to attack."
)

W001 = Warning(
    "You do not have 'django.middleware.security.SecurityMiddleware' "
    "in your MIDDLEWARE so the SECURE_HSTS_SECONDS, "
    "SECURE_CONTENT_TYPE_NOSNIFF, SECURE_REFERRER_POLICY, "
    "SECURE_CROSS_ORIGIN_OPENER_POLICY, and SECURE_SSL_REDIRECT settings will "
    "have no effect.",
    id="security.W001",
)

W002 = Warning(
    "You do not have "
    "'django.middleware.clickjacking.XFrameOptionsMiddleware' in your "
    "MIDDLEWARE, so your pages will not be served with an "
    "'x-frame-options' header. Unless there is a good reason for your "
    "site to be served in a frame, you should consider enabling this "
    "header to help prevent clickjacking attacks.",
    id="security.W002",
)

W004 = Warning(
    "You have not set a value for the SECURE_HSTS_SECONDS setting. "
    "If your entire site is served only over SSL, you may want to consider "
    "setting a value and enabling HTTP Strict Transport Security. "
    "Be sure to read the documentation first; enabling HSTS carelessly "
    "can cause serious, irreversible problems.",
    id="security.W004",
)

W005 = Warning(
    "You have not set the SECURE_HSTS_INCLUDE_SUBDOMAINS setting to True. "
    "Without this, your site is potentially vulnerable to attack "
    "via an insecure connection to a subdomain. Only set this to True if "
    "you are certain that all subdomains of your domain should be served "
    "exclusively via SSL.",
    id="security.W005",
)

W006 = Warning(
    "Your SECURE_CONTENT_TYPE_NOSNIFF setting is not set to True, "
    "so your pages will not be served with an "
    "'X-Content-Type-Options: nosniff' header. "
    "You should consider enabling this header to prevent the "
    "browser from identifying content types incorrectly.",
    id="security.W006",
)

W008 = Warning(
    "Your SECURE_SSL_REDIRECT setting is not set to True. "
    "Unless your site should be available over both SSL and non-SSL "
    "connections, you may want to either set this setting True "
    "or configure a load balancer or reverse-proxy server "
    "to redirect all connections to HTTPS.",
    id="security.W008",
)

W009 = Warning(
    SECRET_KEY_WARNING_MSG % "SECRET_KEY",
    id="security.W009",
)

W018 = Warning(
    "You should not have DEBUG set to True in deployment.",
    id="security.W018",
)

W019 = Warning(
    "You have "
    "'django.middleware.clickjacking.XFrameOptionsMiddleware' in your "
    "MIDDLEWARE, but X_FRAME_OPTIONS is not set to 'DENY'. "
    "Unless there is a good reason for your site to serve other parts of "
    "itself in a frame, you should change it to 'DENY'.",
    id="security.W019",
)

W020 = Warning(
    "ALLOWED_HOSTS must not be empty in deployment.",
    id="security.W020",
)

W021 = Warning(
    "You have not set the SECURE_HSTS_PRELOAD setting to True. Without this, "
    "your site cannot be submitted to the browser preload list.",
    id="security.W021",
)

W022 = Warning(
    "You have not set the SECURE_REFERRER_POLICY setting. Without this, your "
    "site will not send a Referrer-Policy header. You should consider "
    "enabling this header to protect user privacy.",
    id="security.W022",
)

E023 = Error(
    "You have set the SECURE_REFERRER_POLICY setting to an invalid value.",
    hint="Valid values are: {}.".format(", ".join(sorted(REFERRER_POLICY_VALUES))),
    id="security.E023",
)

E024 = Error(
    "You have set the SECURE_CROSS_ORIGIN_OPENER_POLICY setting to an invalid "
    "value.",
    hint="Valid values are: {}.".format(
        ", ".join(sorted(CROSS_ORIGIN_OPENER_POLICY_VALUES)),
    ),
    id="security.E024",
)

W025 = Warning(SECRET_KEY_WARNING_MSG, id="security.W025")

E026 = Error(
    "The Content Security Policy setting '%s' must be a dictionary (got %r instead).",
    id="security.E026",
)


def _security_middleware():
    return "django.middleware.security.SecurityMiddleware" in settings.MIDDLEWARE


def _xframe_middleware():
    return (
        "django.middleware.clickjacking.XFrameOptionsMiddleware" in settings.MIDDLEWARE
    )


@register(Tags.security, deploy=True)
def check_security_middleware(app_configs, **kwargs):
    passed_check = _security_middleware()
    return [] if passed_check else [W001]


@register(Tags.security, deploy=True)
def check_xframe_options_middleware(app_configs, **kwargs):
    passed_check = _xframe_middleware()
    return [] if passed_check else [W002]


@register(Tags.security, deploy=True)
def check_sts(app_configs, **kwargs):
    passed_check = not _security_middleware() or settings.SECURE_HSTS_SECONDS
    return [] if passed_check else [W004]


@register(Tags.security, deploy=True)
def check_sts_include_subdomains(app_configs, **kwargs):
    passed_check = (
        not _security_middleware()
        or not settings.SECURE_HSTS_SECONDS
        or settings.SECURE_HSTS_INCLUDE_SUBDOMAINS is True
    )
    return [] if passed_check else [W005]


@register(Tags.security, deploy=True)
def check_sts_preload(app_configs, **kwargs):
    passed_check = (
        not _security_middleware()
        or not settings.SECURE_HSTS_SECONDS
        or settings.SECURE_HSTS_PRELOAD is True
    )
    return [] if passed_check else [W021]


@register(Tags.security, deploy=True)
def check_content_type_nosniff(app_configs, **kwargs):
    passed_check = (
        not _security_middleware() or settings.SECURE_CONTENT_TYPE_NOSNIFF is True
    )
    return [] if passed_check else [W006]


@register(Tags.security, deploy=True)
def check_ssl_redirect(app_configs, **kwargs):
    passed_check = not _security_middleware() or settings.SECURE_SSL_REDIRECT is True
    return [] if passed_check else [W008]


def _check_secret_key(secret_key):
    return (
        len(set(secret_key)) >= SECRET_KEY_MIN_UNIQUE_CHARACTERS
        and len(secret_key) >= SECRET_KEY_MIN_LENGTH
        and not secret_key.startswith(SECRET_KEY_INSECURE_PREFIX)
    )


@register(Tags.security, deploy=True)
def check_secret_key(app_configs, **kwargs):
    try:
        secret_key = settings.SECRET_KEY
    except (ImproperlyConfigured, AttributeError):
        passed_check = False
    else:
        passed_check = _check_secret_key(secret_key)
    return [] if passed_check else [W009]


@register(Tags.security, deploy=True)
def check_secret_key_fallbacks(app_configs, **kwargs):
    warnings = []
    try:
        fallbacks = settings.SECRET_KEY_FALLBACKS
    except (ImproperlyConfigured, AttributeError):
        warnings.append(Warning(W025.msg % "SECRET_KEY_FALLBACKS", id=W025.id))
    else:
        for index, key in enumerate(fallbacks):
            if not _check_secret_key(key):
                warnings.append(
                    Warning(W025.msg % f"SECRET_KEY_FALLBACKS[{index}]", id=W025.id)
                )
    return warnings


@register(Tags.security, deploy=True)
def check_debug(app_configs, **kwargs):
    passed_check = not settings.DEBUG
    return [] if passed_check else [W018]


@register(Tags.security, deploy=True)
def check_xframe_deny(app_configs, **kwargs):
    passed_check = not _xframe_middleware() or settings.X_FRAME_OPTIONS == "DENY"
    return [] if passed_check else [W019]


@register(Tags.security, deploy=True)
def check_allowed_hosts(app_configs, **kwargs):
    return [] if settings.ALLOWED_HOSTS else [W020]


@register(Tags.security, deploy=True)
def check_referrer_policy(app_configs, **kwargs):
    if _security_middleware():
        if settings.SECURE_REFERRER_POLICY is None:
            return [W022]
        # Support a comma-separated string or iterable of values to allow
        # fallback.
        if isinstance(settings.SECURE_REFERRER_POLICY, str):
            values = {v.strip() for v in settings.SECURE_REFERRER_POLICY.split(",")}
        else:
            values = set(settings.SECURE_REFERRER_POLICY)
        if not values <= REFERRER_POLICY_VALUES:
            return [E023]
    return []


@register(Tags.security, deploy=True)
def check_cross_origin_opener_policy(app_configs, **kwargs):
    if (
        _security_middleware()
        and settings.SECURE_CROSS_ORIGIN_OPENER_POLICY is not None
        and settings.SECURE_CROSS_ORIGIN_OPENER_POLICY
        not in CROSS_ORIGIN_OPENER_POLICY_VALUES
    ):
        return [E024]
    return []


@register(Tags.security)
def check_csp_settings(app_configs, **kwargs):
    """
    Validate that CSP settings are properly configured when enabled.

    Ensures both SECURE_CSP and SECURE_CSP_REPORT_ONLY are dictionaries.
    """
    # CSP settings must be a dictionary or None.
    return [
        Error(E026.msg % (name, value), id=E026.id)
        for name in ("SECURE_CSP", "SECURE_CSP_REPORT_ONLY")
        if (value := getattr(settings, name, None)) is not None
        and not isinstance(value, dict)
    ]
