"""Caddy reverse proxy, CrowdSec security, swag containers and Cloudflare tunnel support"""

# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/01_proxy.ipynb.

# %% auto #0
__all__ = ['Caddyfile', 'caddy', 'caddy_api', 'caddy_svc', 'cloudflared_svc', 'crowdsec']

# %% ../nbs/01_proxy.ipynb #e67b555b
from fastcore.all import L, store_attr, listify, joins, Path
from .core import *

# %% ../nbs/01_proxy.ipynb #fe6aaa0167bca61e
class Caddyfile(L):
	'Fluent builder for production-ready Caddyfiles'
	def __init__(self, domain, app='app', port=5001):
		store_attr()
		super().__init__([])

	def _new(self, items):
		c = type(self)(self.domain, self.app, self.port)
		c.items = items
		return c

	def _add(self, *s): return self._new(self.items + list(s))
	def _has(self, tag): return any(t == tag for t, _ in self)
	def _get(self, tag): return L(self).filter(lambda x: x[0] == tag).itemgot(1)
	def __repr__(self): return str(self)

	def email(self, addr):
		'ACME account email (global). Caddy works without it but uses anonymous account.'
		return self._add(('g', f'email {addr}'))

	def acme_dns(self, provider, token_env=None):
		'DNS-01 TLS challenge — needed when port 80 is unavailable (e.g. behind Cloudflare tunnel)'
		token = f'{{${token_env}}}' if token_env else ''
		return self._add(('s', f'\ttls {{\n\t\tacme_dns {provider} {token}\n\t}}'))

	def cloudflared(self):
		'Use http:// prefix — required when running behind cloudflared tunnel (no public port 80/443)'
		return self._add(('f', 'cloudflared'))

	def crowdsec(self, api_url='http://crowdsec:8080', key_env='CROWDSEC_API_KEY'):
		'CrowdSec bouncer — plugin, not built-in'
		return self._add(('s', f'\tcrowdsec {{\n\t\tapi_url {api_url}\n\t\tapi_key {{${key_env}}}\n\t}}'))

	def rate_limit(self, requests='100', window='1m'):
		'Rate limiting'
		return self._add(('s', f'\trate_limit {{\n\t\tzone dynamic {{\n\t\t\tkey {{http.request.remote_ip}}\n\t\t\tevents {requests}\n\t\t\twindow {window}\n\t\t}}\n\t}}'))

	def max_body(self, size='100m'):
		'Request body limit'
		return self._add(('s', f'\trequest_body {{\n\t\tmax_size {size}\n\t}}'))

	def spa(self, static_dir='/app/static'):
		'SPA preset: static files with fallback to index.html'
		return self._add(('spa', f'\troot * {static_dir}\n\ttry_files {{path}} /index.html\n\tfile_server'))

	def encode(self):
		'Compression — Caddy does NOT enable this by default, one word is enough'
		return self._add(('s', '\tencode'))

	def log(self, output='stderr'):
		'Access logging. Caddy logs errors by default; access logs are opt-in.'
		out = f'output {output}' if output != 'stderr' else ''
		return self._add(('s', f'\tlog {{\n\t\t{out}\n\t}}' if out else '\tlog'))

	def __str__(self):
		'Render full Caddyfile'
		parts = []
		g = list(self._get('g'))
		if g: parts.append('{\n\t' + '\n\t'.join(g) + '\n}')
		prefix = 'http://' if self._has('f') else ''
		site = [f'{prefix}{self.domain} {{'] + list(self._get('s'))
		if self._has('spa'): site += list(self._get('spa'))
		else: site.append(f'\treverse_proxy {self.app}:{self.port}')
		site.append('}')
		parts.append('\n'.join(site))
		return '\n'.join(parts)

	def save(self, path='Caddyfile'):
		'Write to disk'
		Path(path).write_text(str(self))
		return self

# %% ../nbs/01_proxy.ipynb #d1a11e12
def caddy(domain,  # domain to serve (e.g. example.com or sub.example.com)
          app='app',  # upstream app name (must match Compose service name)
          port=5001,  # upstream app port
          email=None,  # ACME account email (optional but good practice)
          dns=None,  # DNS-01 provider name for TLS when port 80 is blocked (e.g. 'cloudflare' or 'duckdns')
          dns_token_env=None,  # env var name for DNS API token (default: {DNS}_API_TOKEN, e.g. CLOUDFLARE_API_TOKEN)
          crowdsec=False,  # enable CrowdSec bouncer plugin (not built-in, requires separate service). you don't need this if you're using the pre-built Caddy images with CrowdSec support.
          crowdsec_url='http://crowdsec:8080',  # CrowdSec API URL (default assumes separate 'crowdsec' service in same Compose network)
          cloudflared=False,  # prefix with http:// for Cloudflare tunnel setups (disables ACME, requires separate cloudflared service)
          encode=False,  # enable compression (not on by default)
          access_log=False  # enable access logging to stderr (errors only by default)
          )->Caddyfile:
	'Minimal Caddyfile for reverse-proxying app:port from domain. Optional ACME email, DNS-01 provider, CrowdSec protection, and Cloudflare tunnel support.'
	cf = Caddyfile(domain, app, port)
	if email: cf = cf.email(email)
	if dns and not cloudflared: cf = cf.acme_dns(dns, dns_token_env or f'{dns.upper()}_API_TOKEN')
	if cloudflared: cf = cf.cloudflared()
	if crowdsec: cf = cf.crowdsec(api_url=crowdsec_url)
	if encode: cf = cf.encode()
	if access_log: cf = cf.log()
	return cf

def caddy_api(domain,
              app='app',
              port=5001,
              email=None,
              dns=None,
              dns_token_env=None,
              cloudflared=False,
              crowdsec=False,
              crowdsec_url='http://crowdsec:8080',
              max_body='50m',
              rate='200',
              rate_window='1m'
              ) -> str:
	'Caddyfile preset for API services — adds rate limiting and body size cap'
	return caddy(domain, app, port, email=email, dns=dns, dns_token_env=dns_token_env, cloudflared=cloudflared,
        crowdsec=crowdsec, crowdsec_url=crowdsec_url).rate_limit(rate, rate_window).max_body(max_body).encode()

# %% ../nbs/01_proxy.ipynb #2aaa95f8
def _caddy_img(crowdsec, cloudflare):
    if crowdsec and cloudflare: return 'ghcr.io/buildplan/csdp-caddy:latest'
    elif crowdsec: return 'serfriz/caddy-crowdsec:latest'
    elif cloudflare: return 'serfriz/caddy-cloudflare:latest'
    else: return 'caddy:2'

def caddy_svc(domain, app='app', port=5001, *, dns=None, email=None,
              crowdsec=False, cloudflared=False, conf='Caddyfile', **kw):
    'Write Caddyfile and return Caddy service kwargs for Compose.svc()'
    caddy(domain, app, port, email=email, dns=dns, cloudflared=cloudflared, crowdsec=crowdsec).save(conf)
    is_cf = dns == 'cloudflare'
    tok = f'{dns.upper()}_API_TOKEN' if dns else None
    env = {}
    if dns: env[tok] = '${%s}' % tok
    if crowdsec: env['CROWDSEC_API_KEY'] = '${CROWDSEC_API_KEY}'
    conf_path = Path(conf)
    vol_key = str(conf_path.resolve()) if conf_path.is_absolute() else f'./{conf_path.name}'
    return service(
        image=kw.pop('image', _caddy_img(crowdsec, is_cf)),
        ports=None if cloudflared else ['80:80', '443:443', '443:443/udp'],
        env=env or None,
        volumes={vol_key: '/etc/caddy/Caddyfile', 'caddy_data': '/data', 'caddy_config': '/config'},
        depends_on=[app], networks=['web'], restart='unless-stopped', **kw
    )

# %% ../nbs/01_proxy.ipynb #52a33e28
def cloudflared_svc(token_env='${CF_TUNNEL_TOKEN}', url=None, **kw):
    'Cloudflare tunnel service kwargs for Compose.svc(). Pass url to set ingress inline (e.g. url="http://caddy").'
    cmd = f'tunnel --no-autoupdate run{" --url " + url if url else ""}'
    return service(image='cloudflare/cloudflared:latest', command=cmd,
                   env={'TUNNEL_TOKEN': token_env}, networks=['web'], restart='unless-stopped', **kw)

# %% ../nbs/01_proxy.ipynb #a63a6718
def crowdsec(collections=None, bouncer_key_env='CROWDSEC_BOUNCER_KEY', **kw):
    'CrowdSec agent service kwargs for Compose.svc()'
    cols = joins(' ', listify(collections or ['crowdsecurity/linux', 'crowdsecurity/caddy', 'crowdsecurity/http-cve']))
    return service(
        image='crowdsecurity/crowdsec:latest',
        env={'COLLECTIONS': cols, 'BOUNCER_KEY_caddy': f'${{{bouncer_key_env}}}'},
        volumes={'crowdsec-db': '/var/lib/crowdsec/data', 'crowdsec-config': '/etc/crowdsec'},
        networks=['web'], restart='unless-stopped', **kw
    )
