"""Bounded copies only; malformed JSON never falls back to storing raw bytes."""
import json
import math
import re
from urllib.parse import urlsplit, urlunsplit, parse_qsl, urlencode

MASK = '[REDACTED]'
MAX_BODY = 65536
URL = re.compile(r'(?:https?|rtsps?|postgres(?:ql)?|redis)://[^\s\"<>]+', re.I)
SECRET = re.compile(r'(password|passwd|authorization|cookie|privatekey|apikey|secret|token)', re.I)
ASSIGN = re.compile(r'''(?ix)(["']?\b[a-z0-9_-]*(?:password|passwd|token|secret|api[_-]?key|authorization|cookie)[a-z0-9_-]*["']?\s*[:=]\s*)(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|[^\s,;&]+)''')
BEARER = re.compile(r'(?i)\b(Bearer|Basic)\s+[A-Za-z0-9._~+/=-]+')
PEM = re.compile(r'-----BEGIN [^-]*PRIVATE KEY-----.*?(?:-----END [^-]*PRIVATE KEY-----|$)', re.S)


def secret_key(key):
    return bool(SECRET.search(re.sub(r'[^a-z0-9]', '', key.lower())))


def text(value):
    def url(match):
        raw = match.group()
        try:
            p = urlsplit(raw)
            pairs = parse_qsl(p.query, keep_blank_values=True)
            changed = p.password is not None or any(secret_key(k) for k, _ in pairs)
            if not changed:
                return raw
            authority = p.netloc
            if p.password is not None:
                authority = authority.split('@')[-1]
                authority = (p.username or '') + ':' + MASK + '@' + authority
            query = urlencode([(k, MASK if secret_key(k) else v) for k, v in pairs])
            return urlunsplit((p.scheme, authority, p.path, query, p.fragment))
        except Exception:
            return '[URL_OMITTED]'
    value = PEM.sub(MASK, value)
    value = URL.sub(url, value)
    return ASSIGN.sub(lambda m: m.group(1) + '"' + MASK + '"', BEARER.sub(MASK, value))


def copy_safe(value):
    marks, nodes, size = [], 0, 0

    def visit(v, path, depth):
        nonlocal nodes, size
        nodes += 1
        if nodes > 4096 or depth > 16 or size > MAX_BODY * 3:
            raise ValueError('TRACE_BODY_LIMIT')
        if isinstance(v, bytes):
            if len(v) > MAX_BODY:
                return {'omitted': 'BODY_LIMIT', 'bytes': len(v)}
            try:
                def pairs(items):
                    out = {}
                    for k, x in items:
                        if k in out:
                            raise ValueError('DUPLICATE_KEY')
                        out[k] = x
                    return out
                v = json.loads(v, object_pairs_hook=pairs,
                               parse_constant=lambda _: (_ for _ in ()).throw(ValueError()))
            except Exception:
                return {'omitted': 'BODY_NOT_SAFE_JSON', 'bytes': len(v)}
            return visit(v, path, depth + 1)
        if isinstance(v, dict):
            out = {}
            for k, x in v.items():
                if not isinstance(k, str) or len(k) > 256:
                    raise ValueError('TRACE_KEY_LIMIT')
                clean_key = text(k)
                p = path + '/' + clean_key
                if secret_key(k):
                    out[clean_key] = MASK
                    if len(marks) < 64:
                        marks.append(p)
                else:
                    out[clean_key] = visit(x, p, depth + 1)
            return out
        if isinstance(v, (list, tuple)):
            return [visit(x, path + '/' + str(i), depth + 1) for i, x in enumerate(v)]
        if isinstance(v, str):
            size += len(v)
            if len(v) > MAX_BODY:
                raise ValueError('TRACE_STRING_LIMIT')
            clean = text(v)
            if clean != v and len(marks) < 64:
                marks.append(path)
            return clean
        if v is None or type(v) in (bool, int):
            return v
        if type(v) is float and math.isfinite(v):
            return v
        return {'omitted': 'UNSUPPORTED_VALUE'}
    try:
        result = visit(value, '', 0)
        encoded = json.dumps(result, ensure_ascii=False, allow_nan=False)
        if len(encoded.encode()) > MAX_BODY * 3:
            raise ValueError('TRACE_RECORD_LIMIT')
        return result, marks
    except Exception:
        return {'omitted': 'SANITIZE_OR_SIZE_LIMIT'}, ['/']
