import os
import re
import json
import hmac
import hashlib
import base64
import time as _time
from urllib.parse import urlparse, urljoin, quote, unquote

import requests
from flask import Flask, request, Response, jsonify

BASE_DIR = os.path.dirname(os.path.abspath(__file__))


def _load_dotenv():
    """Le o .env da VPS sem depender de python-dotenv.

    O painel Pterodactyl desta VPS nao tem o campo de variaveis de ambiente,
    entao a configuracao fica em arquivo. Prioridade: variavel real do
    sistema > .env. Assim quem seta no painel ainda ganha, e o .env serve de
    padrao/editavel pelo File Manager.
    """
    path = os.path.join(BASE_DIR, '.env')
    if not os.path.exists(path):
        return
    try:
        with open(path, encoding='utf-8') as fh:
            for raw in fh:
                line = raw.strip()
                if not line or line.startswith('#'):
                    continue
                if line.startswith('export '):
                    line = line[7:].strip()
                if '=' not in line:
                    continue
                key, _, value = line.partition('=')
                key = key.strip()
                value = value.strip()
                # remove aspas envolventes, se houver
                if len(value) >= 2 and value[0] == value[-1] and value[0] in ('"', "'"):
                    value = value[1:-1]
                # NAO sobrescreve o que ja vier do ambiente (SERVER_PORT etc.)
                os.environ.setdefault(key, value)
    except Exception as exc:
        print(f'[stream] falha ao ler .env: {exc}', flush=True)


_load_dotenv()

app = Flask(__name__)


@app.after_request
def _cors_everywhere(resp):
    """CORS em TODA resposta, inclusive nos erros.

    Antes os headers de CORS so eram setados nos caminhos de sucesso (playlist
    e stream). Um 403 de token expirado saia SEM 'Access-Control-Allow-Origin',
    e o navegador reportava 'bloqueado por CORS' em vez de 'token expirado' --
    o player ficava preso em "Reconectando..." sem nenhuma pista do motivo.
    """
    resp.headers.setdefault('Access-Control-Allow-Origin', '*')
    resp.headers.setdefault(
        'Access-Control-Allow-Headers',
        'Range, Content-Type, If-None-Match, If-Modified-Since, If-Range')
    resp.headers.setdefault(
        'Access-Control-Expose-Headers',
        'Content-Length, Content-Range, Accept-Ranges')
    resp.headers.setdefault('Access-Control-Allow-Methods', 'GET, HEAD, POST, OPTIONS')
    return resp


@app.route('/stream', methods=['OPTIONS'])
@app.route('/health', methods=['OPTIONS'])
def _cors_preflight():
    return ('', 204)


STREAM_TOKEN_EXPIRY = int(os.environ.get('STREAM_TOKEN_EXPIRY', '86400'))  # 24 h (igual ao /proxy do cPanel)
SECRET_KEY = os.environ.get('STREAM_PROXY_SECRET') or os.environ.get('SECRET_KEY') or 'troco-play-stream-secret'
PROXY_CONNECT_TIMEOUT = max(2, int(os.environ.get('PROXY_CONNECT_TIMEOUT', '5') or 5))
PROXY_READ_TIMEOUT = max(5, int(os.environ.get('PROXY_READ_TIMEOUT', '20') or 20))
PROXY_UPSTREAM_TIMEOUT = (PROXY_CONNECT_TIMEOUT, PROXY_READ_TIMEOUT)
PROXY_MAX_STREAM_SECONDS = max(300, int(os.environ.get('PROXY_MAX_STREAM_SECONDS', '10800') or 10800))  # 3h

if SECRET_KEY == 'troco-play-stream-secret':
    print('[stream] AVISO: STREAM_PROXY_SECRET nao definida (usando segredo padrao inseguro).', flush=True)


def _sign_token(payload_b64: str) -> str:
    return hmac.new(SECRET_KEY.encode('utf-8'), payload_b64.encode('utf-8'), hashlib.sha256).hexdigest()


def _verify_stream_token(token: str, user_id=None):
    if not token:
        return None
    try:
        payload_b64, sig = token.rsplit('.', 1)
    except ValueError:
        return None
    try:
        expected = _sign_token(payload_b64)
        if not hmac.compare_digest(sig, expected):
            return None
        payload_bytes = base64.urlsafe_b64decode(payload_b64.encode('utf-8'))
        payload = json.loads(payload_bytes.decode('utf-8'))
    except Exception:
        return None
    if _time.time() > payload.get('exp', 0):
        return None
    if user_id is not None and payload.get('uid') != user_id:
        return None
    return payload


@app.route('/health', methods=['GET'])
def health():
    return jsonify({'status': 'ok', 'ts': _time.time()})


@app.route('/stream', methods=['GET'])
def stream():
    token = request.args.get('token', '')
    payload = _verify_stream_token(token)
    if not payload:
        return 'Token de acesso inválido ou expirado.', 403

    upstream_url = payload.get('url') or ''
    if not upstream_url:
        return 'URL de origem ausente no token.', 400

    try:
        parsed = urlparse(upstream_url)
        if parsed.scheme not in ('http', 'https'):
            return 'Esquema inválido.', 400
    except Exception:
        return 'URL inválida.', 400

    headers = {
        'User-Agent': payload.get('user_agent') or os.environ.get('STREAM_USER_AGENT', 'VLC/3.0'),
        'Accept': '*/*',
        'Accept-Language': 'pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7',
        'Connection': 'keep-alive',
    }
    referrer = payload.get('referrer') or os.environ.get('STREAM_REFERER', '')
    if referrer:
        headers['Referer'] = referrer
        try:
            ru = urlparse(referrer)
            if ru.scheme and ru.netloc:
                headers['Origin'] = f'{ru.scheme}://{ru.netloc}'
        except Exception:
            pass

    for hname in ('Range', 'If-None-Match', 'If-Modified-Since', 'If-Range'):
        if request.headers.get(hname):
            headers[hname] = request.headers[hname]

    r = None
    try:
        r = requests.get(upstream_url, headers=headers, timeout=PROXY_UPSTREAM_TIMEOUT, stream=True)
    except requests.RequestException as exc:
        return f'Erro de conexão com origem: {exc}', 502

    # ── Recuperação de segmento HLS morto ──────────────────────────────────
    # A origem assina a URL de cada segmento com validade curta (~30s). Se o
    # hls.js demorar a percorrer a lista (buffer cheio, retry, aba em segundo
    # plano), o token interno já venceu e a origem responde 404. Sem isso o
    # buffer fica com buracos: o hls.js da "bufferSeekOverHole", fica pulando
    # de um buraco para outro e congela com ct parado em 0.
    #
    # Solucao: se deu 404 e nao e playlist, recarrega a playlist pai (que traz
    # os tokens frescos), acha o MESMO segmento pelo nome e tenta uma vez.
    parent_url = payload.get('parent') or ''
    if r.status_code == 404 and parent_url and '.m3u8' not in urlparse(upstream_url).path.lower():
        fresh = None
        try:
            pr = requests.get(parent_url, headers=headers, timeout=PROXY_UPSTREAM_TIMEOUT)
            pbody = pr.text or ''
            pfinal = pr.url or parent_url
            pr.close()
            if pr.status_code == 200 and ('#EXTM3U' in pbody or '#EXT' in pbody):
                pbase = pfinal.rsplit('/', 1)[0] + '/'
                want = urlparse(unquote(upstream_url)).path.rsplit('/', 1)[-1]
                for line in pbody.splitlines():
                    ls = line.strip()
                    if not ls or ls.startswith('#'):
                        continue
                    if ls.startswith('http://') or ls.startswith('https://'):
                        cand = unquote(ls)
                    else:
                        cand = urljoin(pbase, ls)
                    if cand != upstream_url and urlparse(cand).path.rsplit('/', 1)[-1] == want:
                        fresh = cand
                        break
        except Exception:
            fresh = None

        if fresh:
            try:
                r.close()
            except Exception:
                pass
            try:
                r = requests.get(fresh, headers=headers, timeout=PROXY_UPSTREAM_TIMEOUT, stream=True)
            except requests.RequestException:
                return f'Erro de conexão com origem: {fresh}', 502

    excluded_resp = {'content-length', 'transfer-encoding', 'content-encoding', 'connection'}
    resp_headers = {k: v for k, v in r.headers.items() if k.lower() not in excluded_resp}

    content_type = r.headers.get('Content-Type', 'application/octet-stream')
    url_lower = upstream_url.lower()

    is_m3u8 = (
        'mpegurl' in content_type.lower() or
        url_lower.endswith('.m3u8') or
        '.m3u8?' in url_lower or
        url_lower.endswith('.m3u8/')
    )

    if is_m3u8:
        try:
            body = r.text
        except Exception:
            try:
                body = r.content.decode('utf-8', errors='replace')
            except Exception:
                body = ''

        if not body.strip() or ('#EXTM3U' not in body and '#EXT' not in body):
            return Response(r.content, status=r.status_code, content_type=content_type, headers=resp_headers)

        final_url = r.url if r.url else upstream_url
        try:
            base_url = final_url.rsplit('/', 1)[0] + '/'
        except Exception:
            base_url = final_url + ('/' if not final_url.endswith('/') else '')

        def make_vps_url(target_url: str) -> str:
            try:
                child_payload = {
                    'url': target_url,
                    # A playlist que gerou este filho. Serve para refazer a
                    # resolucao quando a origem devolve 404 por token vencido.
                    'parent': final_url,
                    'uid': payload.get('uid'),
                    'referrer': payload.get('referrer') or '',
                    'user_agent': payload.get('user_agent') or '',
                    'source_index': payload.get('source_index'),
                    'exp': _time.time() + STREAM_TOKEN_EXPIRY,
                }
                pb = base64.urlsafe_b64encode(json.dumps(child_payload, separators=(',', ':')).encode('utf-8')).decode('utf-8')
                tchild = pb + '.' + _sign_token(pb)
                return '/stream?token=' + quote(tchild, safe='')
            except Exception:
                return '/stream?token=' + quote(token, safe='')

        def rewrite_line(line: str) -> str:
            s = line.strip()
            if not s or s.startswith('#'):
                if 'URI="' in s:
                    m = re.search(r'URI="([^"]+)"', s)
                    if m:
                        uri = m.group(1)
                        if not (uri.startswith('http://') or uri.startswith('https://')):
                            uri = urljoin(base_url, uri)
                        else:
                            try:
                                uri = unquote(uri)
                            except Exception:
                                pass
                        s = s[:m.start(1)] + make_vps_url(uri) + s[m.end(1):]
                return s
            if s.startswith('http://') or s.startswith('https://'):
                try:
                    su = unquote(s)
                except Exception:
                    su = s
                return make_vps_url(su)
            resolved = urljoin(base_url, s)
            return make_vps_url(resolved)

        rewritten = '\n'.join(rewrite_line(l) for l in body.splitlines())
        resp_headers['Content-Type'] = 'application/vnd.apple.mpegurl'
        resp_headers['Access-Control-Allow-Origin'] = '*'
        resp_headers['Cache-Control'] = 'no-store'
        resp_headers['X-Accel-Buffering'] = 'no'
        try:
            r.close()
        except Exception:
            pass
        return Response(rewritten, status=200, content_type='application/vnd.apple.mpegurl', headers=resp_headers)

    resp_headers['Access-Control-Allow-Origin'] = '*'
    resp_headers['Access-Control-Allow-Headers'] = 'Range, If-None-Match, If-Modified-Since, If-Range'
    resp_headers['X-Accel-Buffering'] = 'no'
    resp_headers['Cache-Control'] = 'no-store'

    def generate():
        started = _time.time()
        try:
            for chunk in r.iter_content(chunk_size=262144):
                if not chunk:
                    continue
                yield chunk
                if _time.time() - started > PROXY_MAX_STREAM_SECONDS:
                    break
        except GeneratorExit:
            pass
        except Exception:
            pass
        finally:
            try:
                r.close()
            except Exception:
                pass

    return Response(generate(), status=r.status_code, content_type=content_type, headers=resp_headers)


# ─────────────────────────────────────────────────────────────────────────
# Tunel Cloudflare (HTTPS de teste) + partida do servidor
#
# Tudo em Python de proposito: o Pterodactyl desta VPS roda UM comando so
# ("python /home/container/bot.py"). Nao ha como depender de .sh.
# ─────────────────────────────────────────────────────────────────────────
import platform  # noqa: E402
import subprocess  # noqa: E402
import threading  # noqa: E402

CLOUDFLARED_BIN = os.path.join(BASE_DIR, 'cloudflared')
TUNNEL_URL_RE = re.compile(r'https://([a-z0-9][a-z0-9-]*)\.trycloudflare\.com')


def garantir_cloudflared():
    """Baixa o cloudflared se ainda nao existir. Devolve o caminho ou None."""
    if os.path.exists(CLOUDFLARED_BIN):
        try:
            os.chmod(CLOUDFLARED_BIN, 0o755)
            return CLOUDFLARED_BIN
        except Exception:
            pass

    machine = platform.machine().lower()
    if machine in ('x86_64', 'amd64'):
        arch = 'amd64'
    elif machine in ('aarch64', 'arm64'):
        arch = 'arm64'
    else:
        print(f'[tunel] arquitetura nao suportada: {machine}', flush=True)
        return None

    url = f'https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-{arch}'
    print(f'[tunel] baixando cloudflared ({arch})...', flush=True)
    try:
        with requests.get(url, stream=True, timeout=300) as r:
            r.raise_for_status()
            tmp = CLOUDFLARED_BIN + '.part'
            with open(tmp, 'wb') as fh:
                for chunk in r.iter_content(262144):
                    if chunk:
                        fh.write(chunk)
            os.replace(tmp, CLOUDFLARED_BIN)
        os.chmod(CLOUDFLARED_BIN, 0o755)
        return CLOUDFLARED_BIN
    except Exception as exc:
        print(f'[tunel] falha ao baixar cloudflared: {exc}', flush=True)
        return None


def subir_tunel(target):
    """Sobe o cloudflared em background e imprime a URL publica."""
    binario = garantir_cloudflared()
    if not binario:
        print('[tunel] sem cloudflared: o servidor sobe normal, sem HTTPS.', flush=True)
        return None

    log_path = os.path.join(BASE_DIR, 'tunnel.log')
    print(f'[tunel] subindo tunel rapido -> {target}', flush=True)
    proc = subprocess.Popen(
        [binario, 'tunnel', '--no-autoupdate', '--url', target],
        cwd=BASE_DIR,
        stdout=open(log_path, 'wb'),
        stderr=subprocess.STDOUT,
    )

    achado = {}

    def esperar_url():
        # O log repete a URL varias vezes. "api.trycloudflare.com" tambem
        # aparece no arquivo e NAO e a URL do tunel, por isso o filtro.
        for _ in range(60):
            time_sleep = 1
            try:
                with open(log_path, encoding='utf-8', errors='replace') as fh:
                    texto = fh.read()
            except Exception:
                texto = ''
            for sub in TUNNEL_URL_RE.findall(texto):
                if sub.startswith('api'):
                    continue
                achado['url'] = f'https://{sub}.trycloudflare.com'
                break
            if 'url' in achado:
                return
            if proc.poll() is not None:
                return
            threading.Event().wait(time_sleep)

    threading.Thread(target=esperar_url, daemon=True).start()

    for _ in range(62):
        if 'url' in achado:
            print('', flush=True)
            print('=' * 66, flush=True)
            print('  COPIE ESTA URL PARA STREAM_VPS_URL NO CPANEL:', flush=True)
            print(f"  {achado['url']}", flush=True)
            print('  (muda a cada restart do servidor)', flush=True)
            print('=' * 66, flush=True)
            print('', flush=True)
            break
        if proc.poll() is not None:
            print('[tunel] cloudflared diediu. Veja tunnel.log', flush=True)
            break
        threading.Event().wait(1)

    return achado.get('url')


def servir():
    """gunicorn se estiver instalado; senao o servidor embutido do Flask."""
    porta = int(os.environ['SERVER_PORT'])
    workers = int(os.environ.get('STREAM_WORKERS', '2') or 2)
    threads = int(os.environ.get('STREAM_THREADS', '40') or 40)
    timeout = int(os.environ.get('STREAM_TIMEOUT', '3600') or 3600)

    try:
        from gunicorn.app.base import BaseApplication
    except ImportError:
        print('[stream] gunicorn ausente; usando servidor do Flask.', flush=True)
        app.run(host='0.0.0.0', port=porta, threaded=True)
        return

    class _App(BaseApplication):
        def __init__(self, flask_app, opts):
            self.flask_app = flask_app
            self.opts = opts
            super().__init__()

        def load_config(self):
            for key, value in self.opts.items():
                self.cfg.set(key, value)

        def load(self):
            return self.flask_app

    print(f'[stream] porta={porta} workers={workers} threads={threads} timeout={timeout}', flush=True)
    _App(app, {
        'bind': f'0.0.0.0:{porta}',
        'workers': workers,
        'threads': threads,
        'worker_class': 'gthread',
        'timeout': timeout,
        'graceful_timeout': 30,
        'keepalive': 75,
        'accesslog': '-',
        'errorlog': '-',
        'loglevel': 'info',
    }).run()


if __name__ == '__main__':
    # Pterodactyl injeta SERVER_PORT a cada start. NUNCA escrever a porta no
    # codigo: ela muda quando o usuario troca a porta principal no painel.
    porta = int(os.environ['SERVER_PORT'])

    if os.environ.get('TUNNEL_ENABLED', '0') == '1':
        alvo = os.environ.get('TUNNEL_TARGET') or f'http://127.0.0.1:{porta}'
        subir_tunel(alvo)
    else:
        # Sem isto o log fica silencioso e parece que o HTTPS vai funcionar.
        print('[tunel] DESLIGADO (TUNNEL_ENABLED != 1).', flush=True)
        print('[tunel] O navegador vai BLOQUEAR esta porta por mixed content,'
              ' porque o site e HTTPS.', flush=True)
        print('[tunel] Para ligar: ponha TUNNEL_ENABLED=1 em vps/.env e reinicie.',
              flush=True)

    servir()
