#!/usr/bin/env python3
import sys
import os
import json
import base64
import secrets
import urllib.parse
import pkce
from bs4 import BeautifulSoup
import re
import time
from pathlib import Path
import subprocess  # za curl tokenizaciju
import fcntl  # za file locking

code_verifier = None

# Hardcoded credentials
user = "user upisati"
password = "pass upisati"
authFile = '/home/o11/tokens/yetteltv_' + user + '.tokens'
WVD_PATH = '/home/o11/scripts/WVD.wvd'
KEYS_FILE = '/home/o11/keys.txt'
CHANNEL_KEYS_FILE = '/home/o11/channel_keys.json'
ASSET_CHANNEL_MAP_FILE = '/home/o11/asset_channel_map.json'

# PROXY za sve Yettel zahtjeve + curl
PROXY_URL = "proxy upisati"

# User-Agent koji ćemo koristiti i za curl tokenizaciju
CURL_USER_AGENT = (
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
    "AppleWebKit/537.36 (KHTML, like Gecko) "
    "Chrome/130.0.0.0 Safari/537.36"
)

# GLOBALNE VARIJABLE ZA LOGIN COOLDOWN
LAST_LOGIN_FILE = "/home/o11/logs/.yettel_last_login"
LOGIN_COOLDOWN_SECONDS = 300  # 5 minuta - minimalni razmak između login-a
LOGIN_LOCK_FILE = "/home/o11/logs/.yettel_login_lock"

# Parse command line parameters
def parse_params(args, key):
    for arg in args:
        if arg.startswith(key + '='):
            return arg.split('=', 1)[1]
    return None

id = parse_params(sys.argv, 'id')
action = parse_params(sys.argv, 'action')
cdm_mode = parse_params(sys.argv, 'cdm')
challenge = parse_params(sys.argv, 'challenge')
pssh = parse_params(sys.argv, 'pssh')
channel = parse_params(sys.argv, 'channel')
all_channels = parse_params(sys.argv, 'all')

# Simple requests session
import requests
req = requests.Session()

# primijeni globalni proxy na sve requeste
if PROXY_URL:
    req.proxies.update({
        "http": PROXY_URL,
        "https": PROXY_URL,
    })

# ========== POMOĆNE FUNKCIJE ZA LOGIN COOLDOWN ==========

def can_login_now():
    """Provjeri da li možemo raditi login (cooldown period)"""
    if not os.path.exists(LAST_LOGIN_FILE):
        return True
    
    try:
        with open(LAST_LOGIN_FILE, 'r') as f:
            last_login = float(f.read().strip())
        
        elapsed = time.time() - last_login
        if elapsed >= LOGIN_COOLDOWN_SECONDS:
            return True
        else:
            print(f"DEBUG: Login cooldown active. Last login was {elapsed:.0f} seconds ago. Need {LOGIN_COOLDOWN_SECONDS - elapsed:.0f} more seconds.", file=sys.stderr)
            return False
    except:
        return True

def update_last_login_time():
    """Ažuriraj vrijeme zadnjeg login-a"""
    try:
        with open(LAST_LOGIN_FILE, 'w') as f:
            f.write(str(time.time()))
    except:
        pass

def acquire_login_lock():
    """Pokušaj dobiti lock za login (samo jedan proces može login-ovati u isto vrijeme)"""
    try:
        lock_fd = os.open(LOGIN_LOCK_FILE, os.O_CREAT | os.O_WRONLY)
        try:
            fcntl.flock(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
            # Write PID for debugging
            os.write(lock_fd, str(os.getpid()).encode())
            os.fsync(lock_fd)
            return lock_fd
        except (IOError, BlockingIOError):
            os.close(lock_fd)
            return None
    except:
        return None

def release_login_lock(lock_fd):
    """Oslobodi login lock"""
    if lock_fd:
        try:
            fcntl.flock(lock_fd, fcntl.LOCK_UN)
            os.close(lock_fd)
        except:
            pass
        try:
            os.remove(LOGIN_LOCK_FILE)
        except:
            pass

def safe_login():
    """Siguran login sa cooldown-om i lock-om"""
    # Prvo provjeri cooldown
    if not can_login_now():
        print("DEBUG: Login cooldown active, skipping login", file=sys.stderr)
        return "cooldown"
    
    # Pokušaj dobiti lock
    lock_fd = acquire_login_lock()
    if not lock_fd:
        print("DEBUG: Another process is already logging in, waiting...", file=sys.stderr)
        # Sačekaj da drugi proces završi login
        time.sleep(5)
        # Provjeri da li je auth file osvježen
        if os.path.exists(authFile):
            try:
                # Provjeri da li su tokeni sada OK
                auth = json.load(open(authFile))
                channels = get_channels_list(auth)
                if channels and len(channels) > 0:
                    print("DEBUG: Another process refreshed tokens, using them", file=sys.stderr)
                    return "already_refreshed"
            except:
                pass
        return "locked"
    
    try:
        print("DEBUG: Attempting login (cooldown passed, lock acquired)", file=sys.stderr)
        result = login()
        if result == "error":
            print("DEBUG: Login failed", file=sys.stderr)
            return "error"
        
        # Ažuriraj vrijeme zadnjeg login-a
        update_last_login_time()
        print("DEBUG: Login successful, tokens refreshed", file=sys.stderr)
        return "success"
    finally:
        release_login_lock(lock_fd)

# ========== NOVE FUNKCIJE ZA AUTOMATSKO IZVLACENJE KLJUCEVA ZA SVE KANALE ==========

def extract_keys_for_all_channels(auth):
    """
    Automatski izvlači ključeve za SVE kanale
    """
    print("DEBUG: Starting automatic key extraction for ALL channels", file=sys.stderr)

    # Uzmi listu svih kanala
    channels = get_channels_list(auth)
    if not channels:
        print("DEBUG: No channels found", file=sys.stderr)
        return False

    print(f"DEBUG: Found {len(channels)} channels", file=sys.stderr)

    all_extracted_keys = []
    successful_channels = 0
    failed_channels = 0

    # Za svaki kanal pokušaj da izvučeš ključ
    for i, channel_data in enumerate(channels):
        channel_id = channel_data['id']
        channel_name = channel_data['name']

        print(f"\nDEBUG: [{i+1}/{len(channels)}] Extracting keys for channel: {channel_name} (ID: {channel_id})", file=sys.stderr)

        try:
            keys = extract_keys_with_pywidevine(auth, channel_id)

            if keys:
                print(f"DEBUG: SUCCESS - Extracted {len(keys)} keys for {channel_name}", file=sys.stderr)
                all_extracted_keys.extend(keys)
                successful_channels += 1

                # Sačuvaj ključeve za ovaj kanal
                save_key_for_channel(channel_id, keys)

                # Dodaj u cfg fajl odmah
                for key_entry in keys:
                    if ':' in key_entry:
                        kid, key_value = key_entry.split(':', 1)
                        update_yetteltv_cfg_with_key(kid, key_value)
            else:
                print(f"DEBUG: FAILED - No keys extracted for {channel_name}", file=sys.stderr)
                failed_channels += 1

            time.sleep(2)

        except Exception as e:
            print(f"DEBUG: ERROR extracting keys for {channel_name}: {e}", file=sys.stderr)
            failed_channels += 1
            time.sleep(1)

    # Ažuriraj keys.txt sa svim izvučenim ključevima
    if all_extracted_keys:
        update_keys_file(all_extracted_keys)
        print(f"\n=== AUTOMATIC KEY EXTRACTION COMPLETE ===", file=sys.stderr)
        print(f"Total channels processed: {len(channels)}", file=sys.stderr)
        print(f"Successfully extracted keys for: {successful_channels} channels", file=sys.stderr)
        print(f"Failed to extract keys for: {failed_channels} channels", file=sys.stderr)
        print(f"Total unique keys extracted: {len(set(all_extracted_keys))}", file=sys.stderr)
        print(f"CFG file updated: /home/o11/providers/yetteltv.cfg", file=sys.stderr)

        return {
            "status": "success",
            "total_channels": len(channels),
            "successful": successful_channels,
            "failed": failed_channels,
            "total_keys": len(set(all_extracted_keys))
        }
    else:
        print(f"DEBUG: No keys were extracted from any channel", file=sys.stderr)
        return False

def extract_keys_for_channel_range(auth, start_id, end_id):
    """
    Izvlači ključeve za opseg kanala (od start_id do end_id)
    """
    print(f"DEBUG: Extracting keys for channels {start_id} to {end_id}", file=sys.stderr)

    all_extracted_keys = []

    for channel_id in range(start_id, end_id + 1):
        print(f"DEBUG: Trying channel ID: {channel_id}", file=sys.stderr)

        try:
            keys = extract_keys_with_pywidevine(auth, channel_id)
            if keys:
                print(f"DEBUG: Extracted keys for channel {channel_id}", file=sys.stderr)
                all_extracted_keys.extend(keys)

                save_key_for_channel(channel_id, keys)

                for key_entry in keys:
                    if ':' in key_entry:
                        kid, key_value = key_entry.split(':', 1)
                        update_yetteltv_cfg_with_key(kid, key_value)

            time.sleep(1)

        except Exception as e:
            print(f"DEBUG: Error with channel {channel_id}: {e}", file=sys.stderr)
            time.sleep(0.5)

    if all_extracted_keys:
        update_keys_file(all_extracted_keys)
        return len(set(all_extracted_keys))

    return 0

def update_yetteltv_cfg_with_key(kid, key):
    """
    Dodaje ili ažurira ključ u yetteltv.cfg fajlu u formatu: KID=KEY
    """
    cfg_file = '/home/o11/providers/yetteltv.cfg'

    if not os.path.exists(cfg_file):
        print(f"DEBUG: Creating new cfg file: {cfg_file}", file=sys.stderr)
        with open(cfg_file, 'w') as f:
            f.write(f"# Yettel TV Keys Configuration\n")
            f.write(f"# Automatically updated on {time.strftime('%Y-%m-%d %H:%M:%S')}\n\n")

    existing_lines = []
    key_found = False

    if os.path.exists(cfg_file):
        with open(cfg_file, 'r') as f:
            existing_lines = f.readlines()

    new_lines = []
    for line in existing_lines:
        if line.strip() and not line.strip().startswith('#'):
            if '=' in line:
                existing_kid = line.split('=')[0].strip()
                if existing_kid == kid:
                    new_lines.append(f"{kid}={key}\n")
                    key_found = True
                    print(f"DEBUG: Updated existing key {kid} in cfg file", file=sys.stderr)
                else:
                    new_lines.append(line)
            else:
                new_lines.append(line)
        else:
            new_lines.append(line)

    if not key_found:
        new_lines.append(f"{kid}={key}\n")
        print(f"DEBUG: Added new key {kid} to cfg file", file=sys.stderr)

    with open(cfg_file, 'w') as f:
        f.writelines(new_lines)

    return True

def update_cfg_file():
    """
    Automatski ažuriraj yetteltv.cfg fajl sa SVIM ključevima iz keys.txt
    """
    try:
        keys_file = '/home/o11/keys.txt'
        cfg_file = '/home/o11/providers/yetteltv.cfg'

        if not os.path.exists(keys_file):
            print(f"DEBUG: Keys file not found: {keys_file}", file=sys.stderr)
            return False

        keys = []
        with open(keys_file, 'r') as f:
            for line in f:
                line = line.strip()
                if ':' in line:
                    parts = line.split(':', 1)
                    if len(parts) == 2:
                        kid, key_value = parts
                        if len(kid) == 32 and len(key_value) == 32:
                            keys.append((kid, key_value))

        with open(cfg_file, 'w') as f:
            f.write("# Yettel TV Keys Configuration\n")
            f.write(f"# Automatically generated on {time.strftime('%Y-%m-%d %H:%M:%S')}\n")
            f.write(f"# Total keys: {len(keys)}\n\n")

            for kid, key_value in keys:
                f.write(f"{kid}={key_value}\n")

        print(f"DEBUG: Updated {cfg_file} with {len(keys)} keys from {keys_file}", file=sys.stderr)
        return True

    except Exception as e:
        print(f"DEBUG: Error updating cfg file: {e}", file=sys.stderr)
        return False

def save_key_for_channel(channel_id, key_data):
    """
    Čuva izvučene ključeve za specifični kanal
    """
    if os.path.exists(CHANNEL_KEYS_FILE):
        with open(CHANNEL_KEYS_FILE, 'r') as f:
            try:
                channel_keys = json.load(f)
            except:
                channel_keys = {}
    else:
        channel_keys = {}

    if str(channel_id) not in channel_keys:
        channel_keys[str(channel_id)] = []

    for key_entry in key_data:
        if key_entry not in channel_keys[str(channel_id)]:
            channel_keys[str(channel_id)].append(key_entry)

    with open(CHANNEL_KEYS_FILE, 'w') as f:
        json.dump(channel_keys, f, indent=2)

    print(f"DEBUG: Saved key for channel {channel_id}", file=sys.stderr)
    return True

# ========== POSTOJEĆE FUNKCIJE ==========

def do_anonymous_login():
    headers = {
        'accept': 'application/json',
        'content-type': 'application/json',
        'origin': 'https://www.ytv.rs',
        'referer': 'https://www.ytv.rs/',
        'user-agent': CURL_USER_AGENT,
        'x-requested-with': 'XMLHttpRequest',
    }
    params = {'format': '1', 'clientTag': '9.54.0-PC'}
    json_data = {
        'language': '*',
        'partnerId': 3200,
        'clientTag': '9.54.0-PC',
        'apiVersion': '5.4.0',
    }
    response = req.post('https://3200.frp1.ott.kaltura.com/api_v3/service/ottuser/action/anonymousLogin', params=params, headers=headers, json=json_data)
    try:
        data = response.json()
        return data['result']['ks']
    except:
        return None

def get_jwt_token(code, code_verifier):
    headers = {
        'Accept': '*/*',
        'Origin': 'https://www.ytv.rs',
        'Referer': 'https://www.ytv.rs/',
        'User-Agent': CURL_USER_AGENT,
        'content-type': 'application/x-www-form-urlencoded',
        'x-requested-with': 'XMLHttpRequest',
    }
    data = {
        'grant_type': 'authorization_code',
        'client_id': 'tv-public',
        'code': code,
        'redirect_uri': 'https://www.ytv.rs/auth/',
        'code_verifier': code_verifier,
    }
    response = req.post('https://www.yettel.rs/nalog/api/token', headers=headers, data=data)
    try:
        data = response.json()
        return data['access_token'], data['refresh_token']
    except:
        return None, None

def do_token_login(anon_ks, jwt_token, device_id=None):
    if device_id is None:
        device_id = secrets.token_hex(8).upper()
    headers = {
        'accept': 'application/json',
        'content-type': 'application/json',
        'origin': 'https://www.ytv.rs',
        'referer': 'https://www.ytv.rs/',
        'user-agent': CURL_USER_AGENT,
        'x-requested-with': 'XMLHttpRequest',
    }
    params = {'format': '1', 'clientTag': '9.54.0-PC'}
    json_data = {
        'language': 'srp',
        'ks': anon_ks,
        'partnerId': 3200,
        'username': '11111',
        'password': '11111',
        'extraParams': {
            'loginType': {'objectType': 'KalturaStringValue', 'value': 'accessToken'},
            'accessToken': {'objectType': 'KalturaStringValue', 'value': jwt_token},
        },
        'udid': device_id,
        'clientTag': '9.54.0-PC',
        'apiVersion': '5.4.0',
    }
    response = req.post('https://3200.frp1.ott.kaltura.com/api_v3/service/ottuser/action/login', params=params, headers=headers, json=json_data)
    try:
        data = response.json()
        return data['result']['loginSession']['ks']
    except:
        return None

def get_device_id(ks):
    headers = {
        'accept': 'application/json',
        'content-type': 'application/json',
        'origin': 'https://www.ytv.rs',
        'referer': 'https://www.ytv.rs/',
        'user-agent': CURL_USER_AGENT,
        'x-requested-with': 'XMLHttpRequest',
    }
    params = {'format': '1', 'clientTag': '9.54.0-PC'}
    json_data = {
        'language': 'srp',
        'ks': ks,
        'clientTag': '9.54.0-PC',
        'apiVersion': '5.4.0',
    }
    response = req.post('https://3200.frp1.ott.kaltura.com/api_v3/service/householddevice/action/list', params=params, headers=headers, json=json_data)
    try:
        data = response.json()
        return data['result']['objects'][0]['udid']
    except:
        return None

def get_login_cookies(code_challenge):
    headers = {
        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
        'User-Agent': CURL_USER_AGENT,
    }
    params = {
        'response_type': 'code',
        'client_id': 'tv-public',
        'redirect_uri': 'https://www.ytv.rs/auth/',
        'code_challenge': code_challenge,
        'code_challenge_method': 'S256',
        'scope': 'tv_info',
    }
    response = req.get('https://www.yettel.rs/nalog/api/auth', params=params, headers=headers)
    try:
        soup = BeautifulSoup(response.content, 'html.parser')
        csrf_name = soup.find('input', {'name': 'csrf_name'})['value']
        csrf_value = soup.find('input', {'name': 'csrf_value'})['value']
        return csrf_name, csrf_value
    except:
        return None, None

def do_login():
    global code_verifier
    code_verifier, code_challenge = pkce.generate_pkce_pair()
    csrf_name, csrf_value = get_login_cookies(code_challenge)
    if not csrf_name:
        return "error"

    headers = {
        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
        'Content-Type': 'application/x-www-form-urlencoded',
        'Origin': 'https://www.yettel.rs',
        'Referer': 'https://www.yettel.rs/nalog/app/login',
        'User-Agent': CURL_USER_AGENT,
    }
    data = {
        'csrf_name': csrf_name,
        'csrf_value': csrf_value,
        'resource': user,
        'password': password,
        'stayLogin': 'on',
    }
    response = req.post('https://www.yettel.rs/nalog/app/login-post', headers=headers, data=data, allow_redirects=True)
    try:
        final_url = response.url
        if 'code=' in final_url:
            code = final_url.split('code=')[-1].split('&')[0]
            remember_me_id = response.cookies.get('account-logged-in-user', '')
            return remember_me_id, code
        content = response.text
        if 'code=' in content:
            code = content.split('code=')[-1].split('"')[0].split('&')[0]
            remember_me_id = response.cookies.get('account-logged-in-user', '')
            return remember_me_id, code
        return None, None
    except:
        return None, None

def login():
    account_logged_in_user, code = do_login()
    if not code:
        return "error"
    anon_ks = do_anonymous_login()
    if not anon_ks:
        return "error"
    jwt_token, jwt_refresh_token = get_jwt_token(code, code_verifier)
    if not jwt_token:
        return "error"
    ks = do_token_login(anon_ks, jwt_token)
    if not ks:
        return "error"
    device_id = get_device_id(ks)
    if not device_id:
        return "error"
    ks = do_token_login(anon_ks, jwt_token, device_id)
    if not ks:
        return "error"
    auth_data = {
        'account_logged_in_user': account_logged_in_user,
        'ks': ks,
        'jwt_token': jwt_token,
        'jwt_refresh_token': jwt_refresh_token,
        'device_id': device_id
    }
    json.dump(auth_data, open(authFile, 'w'))
    return None

def check_auth_validity():
    """Provjeri da li su tokeni validni - BRZA provjera"""
    try:
        if not os.path.exists(authFile):
            print("DEBUG: No auth file", file=sys.stderr)
            return False
        
        auth = json.load(open(authFile))
        
        # Provjeri samo da li auth file ima sve potrebne fieldove
        required_fields = ['ks', 'jwt_token', 'device_id']
        for field in required_fields:
            if field not in auth or not auth[field]:
                print(f"DEBUG: Missing field in auth: {field}", file=sys.stderr)
                return False
        
        print("DEBUG: Auth file exists and looks valid", file=sys.stderr)
        return True
    except Exception as e:
        print(f"DEBUG: Auth check error: {e}", file=sys.stderr)
        return False

def get_channels_list(auth):
    headers = {
        'accept': '*/*',
        'content-type': 'application/json',
        'origin': 'https://www.ytv.rs',
        'referer': 'https://www.ytv.rs/',
        'user-agent': CURL_USER_AGENT,
    }
    params = {'clientTag': '9.54.0-PC'}
    json_data = {
        'filter': {
            'objectType': 'KalturaSearchAssetFilter',
            'kSql': "(and asset_type='601')",
        },
        'pager': {
            'objectType': 'KalturaFilterPager',
            'pageIndex': 1,
            'pageSize': 500,
        },
        'ks': auth['ks'],
        'language': '',
        'apiVersion': '5.3.5.28003',
    }
    response = req.post('https://3200.frp1.ott.kaltura.com/api_v3/service/asset/action/list', params=params, headers=headers, json=json_data)
    try:
        data = response.json()
        return data['result']['objects']
    except:
        return None

def get_playback_context(auth, asset_id):
    """Get playback context for asset"""
    headers = {
        'accept': '*/*',
        'content-type': 'application/json',
        'origin': 'https://www.ytv.rs',
        'referer': 'https://www.ytv.rs/',
        'user-agent': CURL_USER_AGENT,
    }

    json_data = {
        '1': {
            'service': 'asset',
            'action': 'get',
            'id': asset_id,
            'assetReferenceType': 'media',
            'ks': auth['ks'],
        },
        '2': {
            'service': 'asset',
            'action': 'getPlaybackContext',
            'assetId': asset_id,
            'assetType': 'media',
            'contextDataParams': {
                'objectType': 'KalturaPlaybackContextOptions',
                'context': 'PLAYBACK',
                'streamerType': 'mpegdash',
                'urlType': 'DIRECT',
            },
            'ks': auth['ks'],
        },
        'apiVersion': '7.8.1',
        'ks': auth['ks'],
        'partnerId': 3200,
    }

    try:
        response = req.post(
            'https://3200.frp1.ott.kaltura.com/api_v3/service/multirequest',
            json=json_data,
            headers=headers
        )
        return response.json()
    except Exception as e:
        print(f"Playback context error: {e}", file=sys.stderr)
        return None

def extract_error_text_from_context(ctx) -> str:
    """
    Izvuci realan error iz Kaltura multirequest odgovora.
    Vraca "" ako nema error-a.
    """
    if not ctx or not isinstance(ctx, dict):
        return "empty context"

    res = ctx.get("result")

    # multirequest obicno vraca listu elemenata: [asset.get, asset.getPlaybackContext]
    if isinstance(res, list):
        for item in res:
            if not isinstance(item, dict):
                continue

            # 1) Najcesci format: {"error": {"code": "...", "message": "...", "objectType": "..."}}
            if "error" in item and isinstance(item["error"], dict):
                e = item["error"]
                code = str(e.get("code", "")).strip()
                msg = str(e.get("message", "")).strip()
                ot  = str(e.get("objectType", e.get("objecttype", ""))).strip()

                # Ako stvarno ima error code/message → vrati
                if code or msg:
                    return f"{code} {msg} {ot}".strip()

            # 2) Neki odgovori imaju exception direktno
            ot = str(item.get("objectType", item.get("objecttype", ""))).lower()
            if ot.endswith("exception"):
                code = str(item.get("code", "")).strip()
                msg  = str(item.get("message", "")).strip()
                if code or msg:
                    return f"{code} {msg} {ot}".strip()

        # nema error-a u listi
        return ""

    # Ako je result dict (rjeđe)
    if isinstance(res, dict):
        if "error" in res and isinstance(res["error"], dict):
            e = res["error"]
            code = str(e.get("code", "")).strip()
            msg = str(e.get("message", "")).strip()
            ot  = str(e.get("objectType", e.get("objecttype", ""))).strip()
            if code or msg:
                return f"{code} {msg} {ot}".strip()

        ot = str(res.get("objectType", res.get("objecttype", ""))).lower()
        if ot.endswith("exception"):
            code = str(res.get("code", "")).strip()
            msg  = str(res.get("message", "")).strip()
            if code or msg:
                return f"{code} {msg} {ot}".strip()

        return ""

    # fallback (ne tretiramo kao error)
    return ""

def is_session_expired_error(error_text):
    """Provjeri da li je error zbog isteka sesije/tokena"""
    if not error_text:
        return False
    
    error_lower = error_text.lower()
    
    # Ključne riječi koje ukazuju na istekle tokene
    session_keywords = [
        "invalid session", "session expired", "session invalid",
        "invalid ks", "expired ks", "ks expired", "ks invalid",
        "invalid token", "expired token", "token expired",
        "unauthorized", "access denied", "forbidden",
        "login required", "authentication failed", "auth",
        "not authorized", "no permission", "permission denied"
    ]
    
    # Yettel/Kaltura specific error codes za session
    session_error_codes = [
        "500007", "500011", "500016", "500017", "500018",
        "400003", "400004", "400005"
    ]
    
    # Provjeri ključne riječi
    for keyword in session_keywords:
        if keyword in error_lower:
            print(f"DEBUG: Session error detected via keyword: {keyword}", file=sys.stderr)
            return True
    
    # Provjeri error kodove
    for code in session_error_codes:
        if code in error_text:
            print(f"DEBUG: Session error detected via code: {code}", file=sys.stderr)
            return True
    
    # Ako ima "expired" ili "invalid" zajedno sa "ks" ili "session" ili "token"
    if ("expired" in error_lower or "invalid" in error_lower) and \
       ("ks" in error_lower or "session" in error_lower or "token" in error_lower):
        print(f"DEBUG: Session error detected via combination", file=sys.stderr)
        return True
    
    # Ako je 5xx error (server error) koji može biti session related
    if "500" in error_text:
        print(f"DEBUG: 5xx error, treating as possible session error", file=sys.stderr)
        return True
    
    return False

def process_license_challenge(license_url, challenge_b64):
    """Process license challenge for internal CDM"""
    try:
        if challenge_b64 == "cert":
            return "CAQ="

        challenge_data = base64.b64decode(challenge_b64)

        response = req.post(
            license_url,
            data=challenge_data,
            headers={
                "Content-Type": "application/octet-stream",
                "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
            },
            timeout=30
        )

        if response.status_code == 200:
            return base64.b64encode(response.content).decode('utf-8')
        else:
            return ""

    except Exception as e:
        return ""

def get_keys_from_file():
    """Read all keys from keys.txt file"""
    keys = []
    try:
        with open(KEYS_FILE, 'r') as f:
            for line in f:
                line = line.strip()
                if ':' in line:
                    keys.append(line)
        print(f"DEBUG: Loaded {len(keys)} keys from {KEYS_FILE}", file=sys.stderr)
        return keys
    except Exception as e:
        print(f"DEBUG: Error reading keys file: {e}", file=sys.stderr)
        return None

def extract_pssh_from_content(content):
    """Extract PSSH from manifest content using multiple methods"""
    if '<cenc:pssh>' in content:
        start = content.find('<cenc:pssh>') + len('<cenc:pssh>')
        end = content.find('</cenc:pssh>', start)
        if start > len('<cenc:pssh>') and end > start:
            pssh_b64 = content[start:end].strip()
            print(f"DEBUG: Found PSSH in cenc:pssh: {pssh_b64}", file=sys.stderr)
            return pssh_b64

    pssh_pattern = r'<pssh[^>]*>([^<]+)</pssh>'
    matches = re.findall(pssh_pattern, content, re.IGNORECASE)
    if matches:
        pssh_b64 = matches[0].strip()
        print(f"DEBUG: Found PSSH in pssh tag: {pssh_b64}", file=sys.stderr)
        return pssh_b64

    if 'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed' in content:
        widevine_sections = re.findall(r'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed[^>]*>([^<]+)<', content)
        for section in widevine_sections:
            section = section.strip()
            if len(section) > 50:
                try:
                    decoded = base64.b64decode(section)
                    if b'pssh' in decoded:
                        print(f"DEBUG: Found PSSH in Widevine section: {section}", file=sys.stderr)
                        return section
                except:
                    continue

    base64_pattern = r'[A-Za-z0-9+/=]{50,}'
    matches = re.findall(base64_pattern, content)
    for match in matches:
        if len(match) > 50:
            try:
                decoded = base64.b64decode(match)
                if len(decoded) > 12 and decoded[4:8] == b'pssh':
                    print(f"DEBUG: Found PSSH via base64 search: {match}", file=sys.stderr)
                    return match
            except:
                continue

    return None

def extract_pssh_from_manifest_direct(manifest_url):
    """Extract PSSH from manifest with proper headers to avoid 418 error"""
    try:
        print(f"DEBUG: Downloading manifest with proper headers: {manifest_url}", file=sys.stderr)

        headers = {
            'User-Agent': CURL_USER_AGENT,
            'Accept': '*/*',
            'Accept-Language': 'en-US,en;q=0.9',
            'Origin': 'https://www.ytv.rs',
            'Referer': 'https://www.ytv.rs/',
            'DNT': '1',
            'Connection': 'keep-alive',
            'Sec-Fetch-Dest': 'empty',
            'Sec-Fetch-Mode': 'cors',
            'Sec-Fetch-Site': 'cross-site',
        }

        response = req.get(manifest_url, headers=headers, timeout=10)
        print(f"DEBUG: Manifest response status: {response.status_code}", file=sys.stderr)

        if response.status_code == 200:
            content = response.text
            print(f"DEBUG: Manifest size: {len(content)} bytes", file=sys.stderr)

            with open('/tmp/last_manifest.mpd', 'w') as f:
                f.write(content)

            pssh_b64 = extract_pssh_from_content(content)
            if pssh_b64:
                return pssh_b64
            else:
                print("DEBUG: No PSSH found in manifest content", file=sys.stderr)
                return None
        else:
            print(f"DEBUG: Failed to download manifest: {response.status_code}", file=sys.stderr)
            return None

    except Exception as e:
        print(f"DEBUG: PSSH extraction error: {e}", file=sys.stderr)
        return None

def get_actual_pssh_for_asset(auth, asset_id):
    """Get actual PSSH for asset by extracting from manifest"""
    print(f"DEBUG: Getting actual PSSH for asset {asset_id}", file=sys.stderr)

    context = get_playback_context(auth, asset_id)
    if not context:
        print("DEBUG: Failed to get playback context", file=sys.stderr)
        return None

    manifest_url = None
    try:
        if 'result' in context and len(context['result']) > 1:
            playback_data = context['result'][1]
            if 'sources' in playback_data and playback_data['sources']:
                source = playback_data['sources'][0]
                manifest_url = source.get('url')
                print(f"DEBUG: Found manifest URL: {manifest_url}", file=sys.stderr)
    except Exception as e:
        print(f"DEBUG: Manifest URL extraction error: {e}", file=sys.stderr)

    if manifest_url:
        pssh_b64 = extract_pssh_from_manifest_direct(manifest_url)
        if pssh_b64:
            print(f"DEBUG: Successfully extracted PSSH: {pssh_b64}", file=sys.stderr)
            return pssh_b64
        else:
            print("DEBUG: Failed to extract PSSH from manifest", file=sys.stderr)
            return None
    else:
        print("DEBUG: No manifest URL found", file=sys.stderr)
        return None

def update_keys_file(keys):
    """Update keys.txt file with extracted keys"""
    try:
        if not keys:
            return False

        existing_keys = set()

        if os.path.exists(KEYS_FILE):
            with open(KEYS_FILE, 'r') as f:
                for line in f:
                    line = line.strip()
                    if ':' in line:
                        existing_keys.add(line)

        new_keys = set(keys)
        all_keys = existing_keys.union(new_keys)

        with open(KEYS_FILE, 'w') as f:
            for key in sorted(all_keys):
                f.write(key + '\n')

        update_cfg_file()

        print(f"DEBUG: Updated {KEYS_FILE} with {len(new_keys)} new keys, total {len(all_keys)} keys", file=sys.stderr)
        return True

    except Exception as e:
        print(f"DEBUG: Error updating keys file: {e}", file=sys.stderr)
        return False

def extract_keys_with_pywidevine(auth, asset_id, pssh_b64=None):
    """Extract Widevine keys using pywidevine"""
    try:
        from pywidevine.device import Device
        from pywidevine.cdm import Cdm
        from pywidevine.pssh import PSSH

        print(f"DEBUG: Using pywidevine for key extraction", file=sys.stderr)

        if not os.path.exists(WVD_PATH):
            print(f"DEBUG: WVD file not found: {WVD_PATH}", file=sys.stderr)
            return None

        device = Device.load(WVD_PATH)
        cdm = Cdm.from_device(device)

        context = get_playback_context(auth, asset_id)
        if not context:
            print("DEBUG: Failed to get playback context", file=sys.stderr)
            return None

        license_url = None
        try:
            if 'result' in context and len(context['result']) > 1:
                playback_data = context['result'][1]
                if 'sources' in playback_data and playback_data['sources']:
                    source = playback_data['sources'][0]
                    if 'drm' in source and source['drm']:
                        for drm in source['drm']:
                            if drm.get('scheme') == 'WIDEVINE_CENC':
                                license_url = drm.get('licenseURL')
                                print(f"DEBUG: Found license URL: {license_url}", file=sys.stderr)
                                break
        except Exception as e:
            print(f"DEBUG: License URL extraction error: {e}", file=sys.stderr)

        if not license_url:
            print("DEBUG: No license URL found", file=sys.stderr)
            return None

        if not pssh_b64:
            print("DEBUG: Attempting to extract actual PSSH from manifest", file=sys.stderr)
            pssh_b64 = get_actual_pssh_for_asset(auth, asset_id)

        if not pssh_b64:
            print("DEBUG: No PSSH found, using fallback PSSH", file=sys.stderr)
            pssh_b64 = "AAAANHBzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAABISENCP4CIl1iSf7QwZ9+Q7e+g="
            print(f"DEBUG: Using fallback PSSH: {pssh_b64}", file=sys.stderr)

        print(f"DEBUG: Using PSSH: {pssh_b64}", file=sys.stderr)

        session_id = cdm.open()
        pssh_obj = PSSH(pssh_b64)

        challenge = cdm.get_license_challenge(session_id, pssh_obj)
        print(f"DEBUG: Generated challenge, size: {len(challenge)} bytes", file=sys.stderr)

        license_response = req.post(
            license_url,
            data=challenge,
            headers={
                "Content-Type": "application/octet-stream",
                "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
            },
            timeout=30
        )

        print(f"DEBUG: License response status: {license_response.status_code}", file=sys.stderr)

        if license_response.status_code != 200:
            print(f"DEBUG: License request failed: {license_response.status_code}", file=sys.stderr)
            cdm.close(session_id)
            return None

        cdm.parse_license(session_id, license_response.content)
        print(f"DEBUG: License parsed successfully", file=sys.stderr)

        keys = []
        for key in cdm.get_keys(session_id):
            if key.type == 'CONTENT':
                keys.append(f"{key.kid.hex}:{key.key.hex()}")
                print(f"DEBUG: Found key: {key.kid.hex}:{key.key.hex()}", file=sys.stderr)

        cdm.close(session_id)

        print(f"DEBUG: Extracted {len(keys)} keys with pywidevine", file=sys.stderr)
        return keys

    except Exception as e:
        print(f"DEBUG: Pywidevine error: {e}", file=sys.stderr)
        return None

def extract_keys_external(auth, asset_id, pssh_b64=None):
    """Extract Widevine keys using external CDM (pywidevine + fallback from file)"""
    print(f"DEBUG: External CDM called for asset {asset_id}", file=sys.stderr)

    keys = extract_keys_with_pywidevine(auth, asset_id, pssh_b64)
    if keys:
        update_keys_file(keys)
        for key_entry in keys:
            if ':' in key_entry:
                kid, key_value = key_entry.split(':', 1)
                update_yetteltv_cfg_with_key(kid, key_value)
        return keys

    print("DEBUG: Falling back to keys file", file=sys.stderr)
    keys = get_keys_from_file()
    if keys:
        print(f"DEBUG: Returning {len(keys)} existing keys from file", file=sys.stderr)
        return keys
    else:
        print("DEBUG: No keys found", file=sys.stderr)
        return None

# =============================== CURL TOKENIZACIJA ===============================

def get_tokenized_url_with_curl(manifest_url: str) -> str:
    """
    Resolves Yettel/Kaltura redirect and returns final tokenizirani MPD URL,
    ista logika kao u Yettel xuione skripti (curl -L -w %{url_effective}).
    Ako nešto pukne, vraća originalni manifest_url.
    Radi preko definisanog PROXY_URL.
    """
    try:
        cmd = [
            "curl",
            "-L",
            "-s",
            "-o", "/dev/null",
            "-w", "%{url_effective}",
            "-H", f"User-Agent: {CURL_USER_AGENT}",
        ]

        if PROXY_URL:
            cmd += ["-x", PROXY_URL]

        cmd.append(manifest_url)

        out = subprocess.check_output(cmd, timeout=30)
        token_url = out.decode().strip()

        if not token_url.startswith("http"):
            raise RuntimeError("curl nije vratio tokenizirani URL")

        return token_url
    except Exception as e:
        try:
            sys.stderr.write(f"DEBUG: get_tokenized_url_with_curl error: {e}\n")
        except Exception:
            pass
        # fallback – bolje i "goli" MPD nego ništa
        return manifest_url

# ========== GLAVNA FUNKCIJA ZA MANIFEST SA AUTO-RESTART ==========
def get_manifest_with_auto_restart(channel_id, max_retries=2):
    """
    Pokušaj dobiti manifest sa auto-restart logikom:
    1. Prvo probaj sa postojećim tokenima
    2. Ako ne radi i error je session-related, pokušaj login (sa cooldown-om)
    3. Ako login uspije, probaj ponovo
    4. Ako ne uspije, vrati None
    """
    for attempt in range(max_retries):
        try:
            print(f"DEBUG: Manifest attempt {attempt + 1} for channel {channel_id}", file=sys.stderr)
            
            # Provjeri da li auth file postoji
            if not os.path.exists(authFile):
                print("DEBUG: No auth file, need login", file=sys.stderr)
                if attempt == 0:  # Samo prvi put pokušaj login
                    result = safe_login()
                    if result != "success":
                        print(f"DEBUG: Login failed with result: {result}", file=sys.stderr)
                        return None
                    time.sleep(2)
                continue
            
            # Učitaj auth
            auth = json.load(open(authFile))
            
            # Pokušaj dobiti playback context
            context = get_playback_context(auth, channel_id)
            
            if not context:
                print("DEBUG: No playback context received", file=sys.stderr)
                if attempt == 0:  # Samo prvi put pokušaj login
                    result = safe_login()
                    if result != "success":
                        print(f"DEBUG: Login failed with result: {result}", file=sys.stderr)
                        return None
                    time.sleep(2)
                continue
            
            # Provjeri da li ima error
            error_text = extract_error_text_from_context(context)
            
            if error_text:
                print(f"DEBUG: Context error: {error_text}", file=sys.stderr)
                
                # Provjeri da li je session error
                is_session_error = is_session_expired_error(error_text)
                print(f"DEBUG: Is session error? {is_session_error}", file=sys.stderr)
                
                # Ako je session error i možemo pokušati login
                if is_session_error and attempt == 0:
                    print("DEBUG: Session expired error detected, attempting login...", file=sys.stderr)
                    result = safe_login()
                    print(f"DEBUG: Login result: {result}", file=sys.stderr)
                    if result == "success":
                        time.sleep(2)
                        continue  # Pokušaj ponovo sa novim tokenima
                    elif result == "cooldown":
                        print("DEBUG: Login cooldown active, cannot login now", file=sys.stderr)
                        return None
                    elif result == "locked":
                        print("DEBUG: Another process is logging in, waiting...", file=sys.stderr)
                        time.sleep(5)
                        continue  # Pokušaj ponovo možda su tokeni osvježeni
                    else:
                        print(f"DEBUG: Login failed with: {result}", file=sys.stderr)
                        return None
                elif not is_session_error:
                    # Nije session error, ne pokušavaj login
                    print(f"DEBUG: Non-session error, not attempting login: {error_text}", file=sys.stderr)
                    return None
                else:
                    # Session error ali već smo pokušali login
                    print(f"DEBUG: Session error but already attempted login", file=sys.stderr)
                    return None
            
            # Pokušaj izvući manifest URL
            try:
                if 'result' in context and len(context['result']) > 1:
                    playback_data = context['result'][1]
                    if isinstance(playback_data, dict) and 'sources' in playback_data and playback_data['sources']:
                        source = playback_data['sources'][0]
                        manifest_url = source.get('url')
                        if manifest_url:
                            print(f"DEBUG: Got manifest URL: {manifest_url[:100]}...", file=sys.stderr)
                            token_url = get_tokenized_url_with_curl(manifest_url)
                            if token_url:
                                print(f"DEBUG: Successfully got tokenized URL", file=sys.stderr)
                                return token_url
                            else:
                                print(f"DEBUG: Failed to tokenize URL", file=sys.stderr)
                    else:
                        print(f"DEBUG: No sources in playback data", file=sys.stderr)
                else:
                    print(f"DEBUG: Invalid context structure", file=sys.stderr)
            except Exception as e:
                print(f"DEBUG: Error extracting manifest URL: {e}", file=sys.stderr)
            
            # Ako nismo dobili manifest URL, možda treba login
            if attempt == 0:
                print("DEBUG: No manifest URL, attempting login...", file=sys.stderr)
                result = safe_login()
                if result != "success":
                    print(f"DEBUG: Login failed: {result}", file=sys.stderr)
                    return None
                time.sleep(2)
            
        except Exception as e:
            print(f"DEBUG: Exception in manifest attempt {attempt + 1}: {e}", file=sys.stderr)
            if attempt == 0:
                time.sleep(1)
    
    print("DEBUG: All manifest attempts failed", file=sys.stderr)
    return None

# =============================== DO_ACTION ===============================

def do_action():
    # Za login akciju, samo pozovi login
    if action == "login":
        result = safe_login()
        if result == "success":
            print('{"status": "success"}')
        else:
            print('{"status": "error", "reason": "' + result + '"}')
        return

    # Za ostale akcije, prvo provjeri da li auth postoji
    if action and action != "login":
        if not check_auth_validity():
            print("DEBUG: Auth not valid, attempting login...", file=sys.stderr)
            result = safe_login()
            if result != "success":
                print("{}")
                return

    if action == "extract_all":
        print("DEBUG: Starting automatic extraction for ALL channels", file=sys.stderr)
        try:
            auth = json.load(open(authFile))
        except:
            if safe_login() != "success":
                print("{}")
                return
            try:
                auth = json.load(open(authFile))
            except:
                print("{}")
                return

        result = extract_keys_for_all_channels(auth)
        if result:
            print(json.dumps(result))
        else:
            print('{"status": "error", "message": "Failed to extract keys"}')
        return

    elif action == "extract_range":
        start_id = parse_params(sys.argv, 'start')
        end_id = parse_params(sys.argv, 'end')

        if not start_id or not end_id:
            print('{"status": "error", "message": "Missing start or end parameter"}')
            return

        try:
            auth = json.load(open(authFile))
        except:
            if safe_login() != "success":
                print("{}")
                return
            try:
                auth = json.load(open(authFile))
            except:
                print("{}")
                return

        print(f"DEBUG: Extracting keys for range {start_id} to {end_id}", file=sys.stderr)
        keys_count = extract_keys_for_channel_range(auth, int(start_id), int(end_id))
        print(f'{{"status": "success", "keys_extracted": {keys_count}}}')
        return

    # Ostale akcije zahtijevaju auth
    try:
        auth = json.load(open(authFile))
    except:
        if safe_login() != "success":
            print("{}")
            return
        try:
            auth = json.load(open(authFile))
        except:
            print("{}")
            return

    if action == "channels":
        channels = get_channels_list(auth)
        if not channels:
            print("{}")
            return

        output = {"Channels": []}
        for chan in channels:
            channel_info = {
                "Name": chan['name'],
                "Mode": "live",
                "SessionManifest": True,
                "ManifestScript": 'id=' + str(chan['id']),
                "CdmType": "widevine",
                "UseCdm": True,
                "Cdm": 'id=' + str(chan['id']),
                "Video": "best",
                "OnDemand": True,
                "SpeedUp": True
            }
            output["Channels"].append(channel_info)

        print(json.dumps(output))
        return

    elif action == "cdm":
        if not id or not cdm_mode:
            print("{}")
            return

        if cdm_mode == "internal" and challenge:
            print("DEBUG: Internal CDM mode", file=sys.stderr)
            context = get_playback_context(auth, id)
            if not context:
                print("{}")
                return

            license_url = None
            try:
                if 'result' in context and len(context['result']) > 1:
                    playback_data = context['result'][1]
                    if 'sources' in playback_data and playback_data['sources']:
                        source = playback_data['sources'][0]
                        if 'drm' in source and source['drm']:
                            for drm in source['drm']:
                                if drm.get('scheme') == 'WIDEVINE_CENC':
                                    license_url = drm.get('licenseURL')
                                    break
            except Exception as e:
                print(f"CDM extraction error: {e}", file=sys.stderr)

            if not license_url:
                print("{}")
                return

            license_response = process_license_challenge(license_url, challenge)
            if license_response:
                print(license_response)
            else:
                print("{}")
            return

        elif cdm_mode == "external":
            print("DEBUG: External CDM mode", file=sys.stderr)
            keys = extract_keys_external(auth, id, pssh)

            if keys:
                output = {"keys": keys}
                print(json.dumps(output))
            else:
                print("{}")
            return

        else:
            print("{}")
            return

    elif action == "manifest":
        if not id:
            print("{}")
            return

        # Koristi novu funkciju sa auto-restart logikom
        token_url = get_manifest_with_auto_restart(id)
        
        if token_url:
            out = {
                "Cdn": [],
                "ManifestUrl": token_url,
                "Headers": {
                    "Manifest": {"User-Agent": "Mozilla/5.0"},
                    "Media": {"User-Agent": "Mozilla/5.0"}
                },
                "Heartbeat": {"Url": "", "Params": "", "PeriodMs": 300000}
            }
            print(json.dumps(out))
            return
        else:
            print("DEBUG: Failed to get manifest after auto-restart attempts", file=sys.stderr)
            print("{}")
            return

    elif action == "update_keys":
        print("DEBUG: Starting automatic keys update", file=sys.stderr)

        channels = get_channels_list(auth)
        if not channels:
            print("{}")
            return

        all_keys = []
        updated_count = 0

        for channel in channels[:20]:
            channel_id = channel['id']
            print(f"DEBUG: Extracting keys for channel {channel_id} - {channel['name']}", file=sys.stderr)

            keys = extract_keys_with_pywidevine(auth, channel_id)
            if keys:
                all_keys.extend(keys)
                updated_count += 1
                print(f"DEBUG: Successfully extracted keys for {channel_id}", file=sys.stderr)
            else:
                print(f"DEBUG: Failed to extract keys for {channel_id}", file=sys.stderr)

            time.sleep(1)

        if all_keys:
            update_keys_file(all_keys)
            result = {
                "status": "success",
                "channels_processed": len(channels[:20]),
                "keys_extracted": updated_count,
                "total_keys": len(set(all_keys))
            }
            print(json.dumps(result))
        else:
            print("{}")
        return

    else:
        print("{}")
        return

# Main execution
if __name__ == "__main__":
    do_action()