import json
import os
import base64
import hashlib
import hmac
import threading
import time
import uuid
from http.cookies import SimpleCookie
from urllib.error import HTTPError, URLError
from urllib.request import urlopen
from urllib.parse import urlsplit
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path

from engine_sdk import EngineClient, EngineClientError
from engine_sdk.sentinel import SentinelClient
from engine_sdk.entropy import EntropyClient

ROOT = Path(__file__).parent


def load_local_env():
    """Load developer-only credentials without committing secrets to source."""
    local_env = ROOT / '.env.local'
    if not local_env.exists():
        return
    try:
        for line in local_env.read_text(encoding='utf-8').splitlines():
            name, separator, value = line.partition('=')
            name = name.strip()
            if separator and name and not name.startswith('#'):
                os.environ[name] = value.strip()
    except OSError:
        return


load_local_env()
DATA = ROOT / 'data'
DB = DATA / 'records.json'
SECURITY_LOG = DATA / 'security-log.jsonl'
ENGINE_URL = os.getenv('TRITROIA_GENERATOR_URL', 'http://127.0.0.1:8765')
ENGINE = EngineClient(ENGINE_URL, timeout=12)
BRAND_VEIL_URL = os.getenv('TRITROIA_BRAND_VEIL_URL', 'http://127.0.0.1:8786')
BRAND_VEIL = EngineClient(BRAND_VEIL_URL, timeout=30)
SENTINEL_URL = os.getenv('TRITROIA_SENTINEL_URL', 'http://127.0.0.1:8787')
SENTINEL = SentinelClient(SENTINEL_URL, timeout=3)
ENTROPY_URL = os.getenv('TRITROIA_ENTROPY_URL', 'http://127.0.0.1:8788')
ENTROPY = EntropyClient(ENTROPY_URL, timeout=3)
DB_LOCK = threading.Lock()
AUTH_USER = os.getenv('TRITROIA_ADMIN_USER', 'admin')
AUTH_PASSWORD = os.getenv('TRITROIA_ADMIN_PASSWORD', 'local-change-me')
AUTH_SECRET = os.getenv('TRITROIA_AUTH_SECRET', AUTH_PASSWORD)
AUTH_TTL_SECONDS = int(os.getenv('TRITROIA_SESSION_TTL', '28800'))
AUTH_ENV = os.getenv('TRITROIA_ENV', 'development').lower()
AUTH_ROLE = os.getenv('TRITROIA_ADMIN_ROLE', 'admin')
LOGIN_LIMIT = 5
LOGIN_WINDOW_SECONDS = 900
LOGIN_LOCK = threading.Lock()
LOGIN_FAILURES = {}
LOGIN_IP_FAILURES = {}
USED_CHALLENGES = {}
ACTIVE_SESSIONS = {}
if AUTH_ENV == 'production' and (AUTH_PASSWORD == 'local-change-me' or AUTH_SECRET == AUTH_PASSWORD):
    raise RuntimeError('production_requires_explicit_auth_secret_and_password')


def read_db():
    DATA.mkdir(exist_ok=True)
    if not DB.exists():
        return {'products': [], 'identities': [], 'verifications': []}
    try:
        return json.loads(DB.read_text(encoding='utf-8'))
    except (OSError, json.JSONDecodeError):
        return {'products': [], 'identities': [], 'verifications': []}


def write_db(db):
    DATA.mkdir(exist_ok=True)
    temp = DB.with_suffix('.tmp')
    temp.write_text(json.dumps(db, ensure_ascii=False, indent=2), encoding='utf-8')
    temp.replace(DB)


def response(handler, status, payload, extra_headers=None):
    body = json.dumps(payload, ensure_ascii=False).encode('utf-8')
    handler.send_response(status)
    handler.send_header('Content-Type', 'application/json; charset=utf-8')
    handler.send_header('Content-Length', str(len(body)))
    handler.send_header('Cache-Control', 'no-store')
    for name, value in (extra_headers or {}).items():
        handler.send_header(name, value)
    handler.end_headers()
    handler.wfile.write(body)


def security_event(handler, event, **details):
    DATA.mkdir(exist_ok=True)
    record = {'at': int(time.time()), 'event': event, 'ip': handler.client_address[0], **details}
    with DB_LOCK:
        with SECURITY_LOG.open('a', encoding='utf-8') as stream:
            stream.write(json.dumps(record, ensure_ascii=False) + '\n')


def auth_token():
    expires = int(time.time()) + AUTH_TTL_SECONDS
    csrf = uuid.uuid4().hex
    sid = uuid.uuid4().hex
    value = f'{AUTH_USER}:{expires}:{csrf}:{sid}'
    signature = hmac.new(AUTH_SECRET.encode('utf-8'), value.encode('utf-8'), hashlib.sha256).hexdigest()
    with LOGIN_LOCK:
        ACTIVE_SESSIONS[sid] = expires
    return base64.urlsafe_b64encode(f'{value}:{signature}'.encode('utf-8')).decode('ascii').rstrip('='), csrf


def login_challenge():
    expires = int(time.time()) + 300
    value = f'{expires}:{uuid.uuid4().hex}'
    signature = hmac.new(AUTH_SECRET.encode('utf-8'), value.encode('utf-8'), hashlib.sha256).hexdigest()
    return base64.urlsafe_b64encode(f'{value}:{signature}'.encode('utf-8')).decode('ascii').rstrip('=')


def valid_login_challenge(value):
    try:
        decoded = base64.urlsafe_b64decode(value + '=' * (-len(value) % 4)).decode('utf-8')
        expires, nonce, signature = decoded.split(':', 2)
        signed = f'{expires}:{nonce}'
        expected = hmac.new(AUTH_SECRET.encode('utf-8'), signed.encode('utf-8'), hashlib.sha256).hexdigest()
        if int(expires) <= int(time.time()) or not hmac.compare_digest(signature, expected):
            return False
        with LOGIN_LOCK:
            now = time.time()
            for key, used_at in list(USED_CHALLENGES.items()):
                if now - used_at > 300:
                    USED_CHALLENGES.pop(key, None)
            if nonce in USED_CHALLENGES:
                return False
            USED_CHALLENGES[nonce] = now
        return True
    except (ValueError, TypeError, UnicodeDecodeError):
        return False


def auth_context(handler):
    cookie = SimpleCookie()
    cookie.load(handler.headers.get('Cookie', ''))
    raw = cookie.get('tritroia_session')
    if not raw:
        return None
    try:
        decoded = base64.urlsafe_b64decode(raw.value + '=' * (-len(raw.value) % 4)).decode('utf-8')
        user, expires, csrf, sid, signature = decoded.split(':', 4)
        value = f'{user}:{expires}:{csrf}:{sid}'
        expected = hmac.new(AUTH_SECRET.encode('utf-8'), value.encode('utf-8'), hashlib.sha256).hexdigest()
        with LOGIN_LOCK:
            active_until = ACTIVE_SESSIONS.get(sid)
            if active_until and active_until <= int(time.time()):
                ACTIVE_SESSIONS.pop(sid, None)
                active_until = None
        if user == AUTH_USER and active_until and hmac.compare_digest(signature, expected):
            return {'user': user, 'role': AUTH_ROLE, 'csrf': csrf, 'sid': sid}
    except (ValueError, TypeError, UnicodeDecodeError):
        pass
    return None


def authenticated(handler):
    return auth_context(handler) is not None


def require_auth(handler):
    if authenticated(handler):
        return True
    response(handler, 401, {'error': 'authentication_required'})
    return False


def require_csrf(handler):
    context = auth_context(handler)
    if not context:
        return False
    supplied = handler.headers.get('X-CSRF-Token', '')
    if not supplied or not hmac.compare_digest(supplied, context['csrf']):
        security_event(handler, 'csrf_rejected', user=context['user'])
        response(handler, 403, {'error': 'csrf_validation_failed'})
        return False
    return True


def login_allowed(handler, user):
    ip = handler.client_address[0]
    key = f'{ip}:{user}'
    now = time.time()
    with LOGIN_LOCK:
        failures = [stamp for stamp in LOGIN_FAILURES.get(key, []) if now - stamp < LOGIN_WINDOW_SECONDS]
        ip_failures = [stamp for stamp in LOGIN_IP_FAILURES.get(ip, []) if now - stamp < LOGIN_WINDOW_SECONDS]
        LOGIN_FAILURES[key] = failures
        LOGIN_IP_FAILURES[ip] = ip_failures
        return len(failures) < LOGIN_LIMIT and len(ip_failures) < LOGIN_LIMIT * 3


def login_failed(handler, user):
    ip = handler.client_address[0]
    key = f'{ip}:{user}'
    with LOGIN_LOCK:
        LOGIN_FAILURES.setdefault(key, []).append(time.time())
        LOGIN_IP_FAILURES.setdefault(ip, []).append(time.time())


def enforce_https(handler):
    if AUTH_ENV != 'production':
        return True
    if handler.headers.get('X-Forwarded-Proto', '').lower() == 'https':
        return True
    response(handler, 400, {'error': 'https_required'})
    return False


def redirect_to_login(handler):
    handler.send_response(302)
    handler.send_header('Location', '/login.html')
    handler.send_header('Cache-Control', 'no-store')
    handler.end_headers()


def body(handler):
    length = int(handler.headers.get('Content-Length', '0'))
    if length > 262144:
        raise ValueError('request_body_too_large')
    return json.loads(handler.rfile.read(length) or b'{}')


def engine_health():
    return ENGINE.status()


def sentinel_decision(event):
    try:
        return SENTINEL.decide(event)
    except Exception:
        return {'ok': False, 'decision': 'QUARANTINE', 'severity': 'high', 'reasons': ['sentinel_unavailable'], 'arbitratedBy': 'tritroia-fail-closed'}


def proxy_engine_asset(handler, path):
    """Keep old relative library links working from the dashboard port."""
    if not path.startswith('/api-library/') or '/' in path[len('/api-library/'):].strip('/'):
        return False
    try:
        with urlopen(f'{ENGINE_URL}{path}', timeout=5) as upstream:
            content = upstream.read()
            content_type = upstream.headers.get('Content-Type', 'application/octet-stream')
        handler.send_response(200)
        handler.send_header('Content-Type', content_type)
        handler.send_header('Content-Length', str(len(content)))
        handler.send_header('Cache-Control', 'no-store')
        handler.end_headers()
        handler.wfile.write(content)
    except (HTTPError, URLError, TimeoutError, OSError):
        handler.send_error(404, 'Arquivo não encontrado.')
    return True


def asset_token(engine, image_path, ttl=900):
    expires = int(time.time()) + ttl
    value = f'{engine}:{expires}:{image_path}'
    signature = hmac.new(AUTH_SECRET.encode('utf-8'), value.encode('utf-8'), hashlib.sha256).hexdigest()
    raw = f'{value}:{signature}'.encode('utf-8')
    return base64.urlsafe_b64encode(raw).decode('ascii').rstrip('=')


def proxy_secure_asset(handler, token):
    try:
        decoded = base64.urlsafe_b64decode(token + '=' * (-len(token) % 4)).decode('utf-8')
        engine, expires, image_path, signature = decoded.split(':', 3)
        value = f'{engine}:{expires}:{image_path}'
        expected = hmac.new(AUTH_SECRET.encode('utf-8'), value.encode('utf-8'), hashlib.sha256).hexdigest()
        if int(expires) <= int(time.time()) or not hmac.compare_digest(signature, expected):
            handler.send_error(404, 'Link expirado ou inválido.')
            return True
        upstream_base = BRAND_VEIL_URL if engine == 'brand-veil' else ENGINE_URL
        allowed = image_path.startswith('/api-library/') if engine == 'instante-unico' else image_path.startswith('/api/artworks/')
        if not allowed or '/' in image_path.split('/')[-1]:
            handler.send_error(404, 'Arquivo não encontrado.')
            return True
        with urlopen(f'{upstream_base}{image_path}', timeout=8) as upstream:
            content = upstream.read()
            content_type = upstream.headers.get('Content-Type', 'application/octet-stream')
        handler.send_response(200)
        handler.send_header('Content-Type', content_type)
        handler.send_header('Content-Length', str(len(content)))
        handler.send_header('Cache-Control', 'private, no-store')
        handler.end_headers()
        handler.wfile.write(content)
    except (ValueError, TypeError, UnicodeDecodeError, HTTPError, URLError, TimeoutError, OSError):
        handler.send_error(404, 'Link expirado ou inválido.')
    return True


class TritroiaHandler(SimpleHTTPRequestHandler):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, directory=str(ROOT), **kwargs)

    def end_headers(self):
        self.send_header('X-Content-Type-Options', 'nosniff')
        self.send_header('X-Frame-Options', 'DENY')
        self.send_header('Referrer-Policy', 'no-referrer')
        self.send_header('Permissions-Policy', 'camera=(), microphone=(), geolocation=()')
        if AUTH_ENV == 'production':
            self.send_header('Strict-Transport-Security', 'max-age=31536000; includeSubDomains')
        super().end_headers()

    def do_OPTIONS(self):
        self.send_response(204)
        self.send_header('Access-Control-Allow-Origin', '*')
        self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
        self.send_header('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-CSRF-Token')
        self.end_headers()

    def do_GET(self):
        path = self.path.split('?', 1)[0]
        if path.startswith('/secure-library/'):
            return proxy_secure_asset(self, path.split('/secure-library/', 1)[1])
        if path == '/api/auth/challenge':
            return response(self, 200, {'challenge': login_challenge(), 'issuedAt': int(time.time())})
        if path == '/api/auth/me':
            context = auth_context(self)
            return response(self, 200, {'authenticated': bool(context), 'user': context['user'] if context else None, 'role': context['role'] if context else None, 'csrfToken': context['csrf'] if context else None})
        if path.startswith('/dashboard') and not enforce_https(self):
            return
        if path.startswith('/dashboard') and not authenticated(self):
            return redirect_to_login(self)
        if path.startswith('/api/') and path != '/api/health' and not enforce_https(self):
            return
        if path.startswith('/api/') and path != '/api/health' and not require_auth(self):
            return
        if proxy_engine_asset(self, path):
            return
        db = read_db()
        if path == '/api/health':
            return response(self, 200, {'status': 'ok', 'service': 'tritroia-platform', 'mode': 'new-isolated', 'engineAdapter': 'instante-unico-studio'})
        if path == '/api/engine/status':
            return response(self, 200, {
                'primary': engine_health(),
                'brandVeil': BRAND_VEIL.status(),
            })
        if path == '/api/engine/presets':
            requested = self.path.split('?', 1)[1] if '?' in self.path else ''
            query = dict(item.split('=', 1) for item in requested.split('&') if '=' in item)
            selected = query.get('engine', 'instante-unico').lower()
            client = BRAND_VEIL if selected in ('brand-veil', 'brand_veil', 'brandveil') else ENGINE
            try:
                return response(self, 200, client.list_presets())
            except Exception as error:
                return response(self, 502, {'error': 'preset_catalog_unavailable', 'message': str(error)})
        if path == '/api/engine/capabilities':
            requested = self.path.split('?', 1)[1] if '?' in self.path else ''
            query = dict(item.split('=', 1) for item in requested.split('&') if '=' in item)
            selected = query.get('engine', 'instante-unico').lower()
            client = BRAND_VEIL if selected in ('brand-veil', 'brand_veil', 'brandveil') else ENGINE
            try:
                return response(self, 200, client.capabilities())
            except Exception as error:
                return response(self, 502, {'error': 'engine_capabilities_unavailable', 'message': str(error)})
        if path == '/api/products':
            return response(self, 200, {'products': db['products']})
        if path == '/api/identities':
            return response(self, 200, {'identities': db['identities']})
        if path == '/api/dashboard':
            return response(self, 200, {'counts': {key: len(db[key]) for key in ('products', 'identities', 'verifications')}, 'engine': engine_health()})
        return super().do_GET()

    def do_POST(self):
        path = self.path.split('?', 1)[0]
        if path == '/api/auth/login':
            try:
                payload = body(self)
                user = str(payload.get('user', '')).strip()
                password = str(payload.get('password', ''))
                started_at = float(payload.get('startedAt', 0) or 0)
                honeypot = str(payload.get('website', ''))
                challenge = str(payload.get('challenge', ''))
                challenge_valid = valid_login_challenge(challenge)
                sentinel = sentinel_decision({'type': 'login_attempt', 'ip': self.client_address[0], 'replayKey': challenge, 'challengeValid': challenge_valid, 'honeypotFilled': bool(honeypot), 'elapsedMs': max(0, int((time.time() - started_at) * 1000)) if started_at else 0, 'credentialFailure': False})
                if sentinel.get('decision') in {'DENY', 'QUARANTINE'}:
                    security_event(self, 'sentinel_login_denied', user=user or 'unknown', decision=sentinel.get('decision'))
                    return response(self, 403, {'error': 'automation_rejected'})
                if honeypot or not challenge_valid or not started_at or time.time() - started_at < 1.2:
                    security_event(self, 'bot_login_rejected', user=user or 'unknown')
                    return response(self, 403, {'error': 'automation_rejected'})
                if not login_allowed(self, user):
                    security_event(self, 'login_rate_limited', user=user)
                    return response(self, 429, {'error': 'login_rate_limited', 'retryAfterSeconds': LOGIN_WINDOW_SECONDS})
                if not hmac.compare_digest(user, AUTH_USER) or not hmac.compare_digest(password, AUTH_PASSWORD):
                    login_failed(self, user)
                    security_event(self, 'login_failed', user=user)
                    return response(self, 401, {'error': 'invalid_credentials'})
                with LOGIN_LOCK:
                    LOGIN_FAILURES.pop(f'{self.client_address[0]}:{user}', None)
                token, csrf = auth_token()
                security_event(self, 'login_succeeded', user=user, role=AUTH_ROLE)
                secure = '; Secure' if AUTH_ENV == 'production' else ''
                response(self, 200, {'authenticated': True, 'user': AUTH_USER, 'role': AUTH_ROLE, 'csrfToken': csrf}, extra_headers={'Set-Cookie': f'tritroia_session={token}; HttpOnly; SameSite=Strict; Path=/; Max-Age={AUTH_TTL_SECONDS}{secure}'})
            except (ValueError, TypeError, json.JSONDecodeError):
                response(self, 400, {'error': 'invalid_request'})
            return
        if path == '/api/auth/logout':
            context = auth_context(self)
            if context:
                with LOGIN_LOCK:
                    ACTIVE_SESSIONS.pop(context['sid'], None)
                security_event(self, 'logout', user=context['user'])
            return response(self, 200, {'authenticated': False}, extra_headers={'Set-Cookie': 'tritroia_session=; HttpOnly; SameSite=Strict; Path=/; Max-Age=0'})
        if path not in ('/api/products', '/api/identities', '/api/verify'):
            return response(self, 404, {'error': 'endpoint_not_found'})
        if not enforce_https(self) or not require_auth(self) or not require_csrf(self):
            return
        try:
            payload = body(self)
            # Serialize read-modify-write transactions while the local store is JSON.
            with DB_LOCK:
                db = read_db()
                if path == '/api/products':
                    name = str(payload.get('name', '')).strip()
                    if not name:
                        return response(self, 422, {'error': 'name_required'})
                    item = {'id': str(uuid.uuid4()), 'name': name, 'category': str(payload.get('category', 'não definida')), 'lot': str(payload.get('lot', 'não definido')), 'status': 'active'}
                    db['products'].insert(0, item)
                    write_db(db)
                    return response(self, 201, {'product': item})
                if path == '/api/identities':
                    product_id = str(payload.get('productId', '')).strip()
                    if not any(item['id'] == product_id for item in db['products']):
                        return response(self, 422, {'error': 'product_required'})
                    prompt = str(payload.get('prompt', 'Identidade visual TRITROIA')).strip()
                    selected_engine = str(payload.get('engine', 'instante-unico')).strip().lower()
                    generator = BRAND_VEIL if selected_engine in ('brand-veil', 'brand_veil', 'brandveil') else ENGINE
                    try:
                        entropy = ENTROPY.issue({'engine': selected_engine, 'operation': 'identity_generation', 'productId': product_id})
                    except RuntimeError as error:
                        security_event(self, 'entropy_core_fallback', engine=selected_engine, detail=str(error))
                        entropy = {'token': uuid.uuid4().hex, 'entropySource': 'tritroia-local-fallback'}
                    generated = generator.generate(
                        prompt,
                        colors=payload.get('colors'),
                        reference_profile=payload.get('referenceProfile'),
                        quantity=payload.get('quantity', 1),
                        preset=payload.get('preset'),
                        framePreset=payload.get('framePreset'),
                        sizePreset=payload.get('sizePreset'),
                        brandMarkEnabled=payload.get('brandMarkEnabled', False),
                        entropyToken=entropy['token'],
                    )
                    asset = generated.get('asset', {})
                    if generator is BRAND_VEIL and str(asset.get('imageUrl', '')).startswith('/'):
                        asset['imageUrl'] = asset_token('brand-veil', asset['imageUrl'])
                        asset['imageUrl'] = f'/secure-library/{asset["imageUrl"]}'
                    elif generator is ENGINE and str(asset.get('imageUrl', '')).startswith('/'):
                        asset['imageUrl'] = asset_token('instante-unico', asset['imageUrl'])
                        asset['imageUrl'] = f'/secure-library/{asset["imageUrl"]}'
                    identity = {'id': str(uuid.uuid4()), 'productId': product_id, 'label': str(payload.get('label', 'Identidade principal')), 'prompt': prompt, 'engine': 'brand-veil' if generator is BRAND_VEIL else 'instante-unico', 'preset': payload.get('preset'), 'engineAssetId': asset.get('id'), 'imageUrl': asset.get('imageUrl'), 'imageHash': asset.get('imageHash'), 'entropyDigest': entropy.get('entropyDigest') or entropy.get('token'), 'status': 'active'}
                    db['identities'].insert(0, identity)
                    write_db(db)
                    return response(self, 201, {'identity': identity, 'engineAsset': asset})
                identity_id = str(payload.get('identityId', '')).strip()
                identity = next((item for item in db['identities'] if item['id'] == identity_id), None)
                verified = bool(identity and identity.get('status') == 'active')
                record = {'id': str(uuid.uuid4()), 'identityId': identity_id, 'verified': verified, 'reason': 'identity_found' if verified else 'identity_not_found'}
                db['verifications'].insert(0, record)
                write_db(db)
                return response(self, 200, {'verification': record})
        except ValueError as error:
            return response(self, 413 if str(error) == 'request_body_too_large' else 400, {'error': str(error)})
        except Exception as error:
            return response(self, 502, {'error': 'engine_or_storage_failed', 'message': str(error)})


if __name__ == '__main__':
    port = int(os.getenv('PORT', '8775'))
    server = ThreadingHTTPServer(('0.0.0.0', port), TritroiaHandler)
    print(f'TRITROIA platform: http://0.0.0.0:{port}/site/')
    try:
        server.serve_forever()
    except KeyboardInterrupt:
        pass
    finally:
        server.server_close()
