# -*- coding: utf-8 -*-
from __future__ import unicode_literals

import json
import re
import time
import uuid
import hashlib
import os
import base64
from datetime import datetime, timedelta, timezone

import requests
import xbmcgui

try:
    from urllib.parse import quote_plus
except ImportError:
    from urllib import quote_plus

USER_AGENT = 'Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0'
BRAZIL_TZ = timezone(timedelta(hours=-3))
WINDOW_ID = 10000
PLUTO_TTL_SECONDS = None
PLUTO_CHANNELS_TTL_SECONDS = 900
PLUTO_EPG_TTL_SECONDS = 60
PLUTO_BOOT_TTL_SECONDS = 240
CACHE_MANIFEST_PROP = 'oneplay.pluto.cache.manifest'
SESSION = requests.Session()


def _window():
    return xbmcgui.Window(WINDOW_ID)



def new_session_id():
    try:
        return base64.urlsafe_b64encode(os.urandom(9)).decode('ascii').rstrip('=')
    except Exception:
        return str(os.times())


def get_session_id():
    try:
        return _window().getProperty('oneplay.pluto.session_id') or ''
    except Exception:
        return ''


def set_session_id(session_id):
    try:
        _window().setProperty('oneplay.pluto.session_id', session_id or '')
    except Exception:
        pass


def clear_session_id():
    try:
        _window().clearProperty('oneplay.pluto.session_id')
    except Exception:
        pass


def ensure_session(session_id=''):
    if isinstance(session_id, dict):
        session_id = session_id.get('psid', '')
    incoming = str(session_id or '').strip()
    current = get_session_id()
    if not incoming:
        incoming = new_session_id()
    if incoming != current:
        clear_session_cache()
        set_session_id(incoming)
    return incoming


def _register_cache_key(key):
    try:
        win = _window()
        prop = _cache_prop_name(key)
        manifest_raw = win.getProperty(CACHE_MANIFEST_PROP)
        manifest = []
        if manifest_raw:
            try:
                manifest = json.loads(manifest_raw)
            except Exception:
                manifest = []
        if prop not in manifest:
            manifest.append(prop)
            win.setProperty(CACHE_MANIFEST_PROP, json.dumps(manifest, separators=(',', ':')))
    except Exception:
        pass


def _cache_prop_name(key):
    try:
        digest = hashlib.md5(repr(key).encode('utf-8')).hexdigest()
    except Exception:
        digest = hashlib.md5(str(key).encode('utf-8')).hexdigest()
    return 'oneplay.pluto.{}'.format(digest)


def _cache_get(key):
    try:
        raw = _window().getProperty(_cache_prop_name(key))
        if not raw:
            return None
        data = json.loads(raw)
        ttl = data.get('ttl', PLUTO_TTL_SECONDS)
        if ttl not in (None, '', False):
            ttl = float(ttl)
            if time.time() - float(data.get('ts', 0)) > ttl:
                try:
                    _window().clearProperty(_cache_prop_name(key))
                except Exception:
                    pass
                return None
        return data.get('value')
    except Exception:
        return None


def _cache_set(key, value, ttl=PLUTO_TTL_SECONDS):
    try:
        payload = json.dumps({'ts': time.time(), 'ttl': ttl, 'value': value}, separators=(',', ':'))
        _window().setProperty(_cache_prop_name(key), payload)
        _register_cache_key(key)
    except Exception:
        pass
    return value


def clear_session_cache():
    try:
        win = _window()
        manifest_raw = win.getProperty(CACHE_MANIFEST_PROP)
        manifest = []
        if manifest_raw:
            try:
                manifest = json.loads(manifest_raw)
            except Exception:
                manifest = []
        for key in manifest:
            try:
                win.clearProperty(key)
            except Exception:
                pass
        try:
            win.clearProperty(CACHE_MANIFEST_PROP)
        except Exception:
            pass
        clear_session_id()
    except Exception:
        pass


def _parse_iso_datetime(value):
    if not value:
        return None
    s = value.strip()
    if s.endswith('Z'):
        s = s[:-1] + '+00:00'
    s = re.sub(r'([+-]\d{2}:\d)(?!\d)', lambda m: m.group(1) + '0', s)
    try:
        dt = datetime.fromisoformat(s)
        if dt.tzinfo is None:
            dt = dt.replace(tzinfo=timezone.utc)
        return dt
    except Exception:
        for fmt in ('%Y-%m-%dT%H:%M:%S.%f%z', '%Y-%m-%dT%H:%M:%S%z'):
            try:
                return datetime.strptime(s, fmt)
            except Exception:
                continue
    return None


def _fetch_boot_data():
    session_id = get_session_id() or ''
    cache_key = ('boot', session_id)
    cached = _cache_get(cache_key)
    if cached is not None:
        return cached
    now_utc = datetime.now(timezone.utc)
    stop_utc = now_utc + timedelta(hours=6)
    from_str = now_utc.strftime('%Y-%m-%dT%H:%M:%SZ')
    to_str = stop_utc.strftime('%Y-%m-%dT%H:%M:%SZ')
    session_token = ''
    stitcher_params = ''
    try:
        client_id = str(uuid.uuid4())
        boot_url = (
            'https://boot.pluto.tv/v4/start?appName=web&appVersion=9.19.0-7a6c115631d945c4f7327de3e03b7c474b692657'
            '&deviceVersion=148.0.0&deviceModel=web&deviceMake=firefox&deviceType=web'
            '&clientID={client_id}&clientModelNumber=1.0.0&channelSlug=5f120e94a5714d00074576a1'
            '&serverSideAds=false&drmCapabilities=widevine%3AL3&blockingMode=&notificationVersion=1'
            '&appLaunchCount=0&lastAppLaunchDate={from_str}&clientTime={to_str}'
        ).format(client_id=client_id, from_str=from_str, to_str=to_str)
        response = SESSION.get(boot_url, headers={'User-Agent': USER_AGENT}, timeout=10)
        response.raise_for_status()
        boot_data = response.json()
        if not isinstance(boot_data, dict):
            boot_data = {}
        session_token = boot_data.get('sessionToken', '') or ''
        stitcher_params = boot_data.get('stitcherParams', '') or ''
    except Exception:
        session_token = ''
        stitcher_params = ''
    return _cache_set(cache_key, (session_token, stitcher_params), ttl=PLUTO_BOOT_TTL_SECONDS)


def _build_stream_url_from_stitched(stitched_url, session_token, stitcher_params):
    if not stitched_url:
        return None
    try:
        base_stream = stitched_url.split('?')[0].replace('/stitch/hls/', '/v2/stitch/hls/')
        query = (stitcher_params or '').lstrip('?&')
        if query:
            stream_url = '{}?{}&jwt={}&masterJWTPassthrough=true&includeExtendedEvents=true&eventVOD=false&CMCD=mtp=1000,ot=m,sf=h'.format(
                base_stream, query, session_token
            )
        else:
            stream_url = '{}?jwt={}&masterJWTPassthrough=true&includeExtendedEvents=true&eventVOD=false&CMCD=mtp=1000,ot=m,sf=h'.format(
                base_stream, session_token
            )
        return stream_url + '|User-Agent=' + quote_plus(USER_AGENT)
    except Exception:
        return None


def _extract_category(channel):
    candidates = [
        channel.get('category'),
        channel.get('categoryName'),
        channel.get('genre'),
        channel.get('section'),
    ]

    category_obj = channel.get('category')
    if isinstance(category_obj, dict):
        candidates.extend([
            category_obj.get('name'),
            category_obj.get('title'),
            category_obj.get('slug')
        ])

    categories = channel.get('categories') or []
    if isinstance(categories, list):
        for item in categories:
            if isinstance(item, dict):
                candidates.extend([item.get('name'), item.get('title'), item.get('slug')])
            elif isinstance(item, str):
                candidates.append(item)

    for candidate in candidates:
        if isinstance(candidate, str) and candidate.strip():
            return candidate.strip()
    return 'Geral'


def _extract_program_metadata(channel, now_utc, include_epg=True):
    if not include_epg:
        return channel.get('name') or '#{}'.format(channel.get('number', '')), ''

    timelines = channel.get('timelines', []) or []
    current_program = None
    next_program = None
    for idx, timeline in enumerate(timelines):
        start = _parse_iso_datetime(timeline.get('start'))
        stop = _parse_iso_datetime(timeline.get('stop'))
        if not start or not stop:
            continue
        if start <= now_utc <= stop:
            ep = timeline.get('episode', {}) or {}
            current_program = {
                'title': ep.get('name', ''),
                'description': ep.get('description', ''),
                'start': start,
            }
            if idx + 1 < len(timelines):
                next_timeline = timelines[idx + 1]
                next_start = _parse_iso_datetime(next_timeline.get('start'))
                next_ep = next_timeline.get('episode', {}) or {}
                next_program = {
                    'title': next_ep.get('name', ''),
                    'description': next_ep.get('description', ''),
                    'start': next_start,
                }
            break

    description = ''
    display_name = channel.get('name') or '#{}'.format(channel.get('number', ''))
    if current_program and current_program.get('title'):
        local_now = current_program['start'].astimezone(BRAZIL_TZ)
        description += '[COLOR yellow][{}] {}[/COLOR]\n({})\n'.format(
            local_now.strftime('%H:%M'), current_program.get('title', ''), current_program.get('description', '')
        )
        display_name = '{} - [COLOR yellow]{}[/COLOR]'.format(display_name, current_program.get('title', ''))
    if next_program and next_program.get('title') and next_program.get('start'):
        local_next = next_program['start'].astimezone(BRAZIL_TZ)
        description += '[COLOR yellow][{}] {}[/COLOR]\n({})\n'.format(
            local_next.strftime('%H:%M'), next_program.get('title', ''), next_program.get('description', '')
        )
    return display_name, description


def _window_for_fetch(include_epg):
    now_utc = datetime.now(timezone.utc)
    stop_utc = now_utc + (timedelta(hours=6) if include_epg else timedelta(hours=1))
    return now_utc, stop_utc


def _channel_key_from_values(slug='', channel_id='', number=0):
    slug = str(slug or '').strip()
    channel_id = str(channel_id or '').strip()
    try:
        number = int(number or 0)
    except Exception:
        number = 0
    if slug:
        return 'slug:' + slug
    if channel_id:
        return 'id:' + channel_id
    return 'num:' + str(number)


def _channel_key(item):
    return _channel_key_from_values(item.get('slug', ''), item.get('channel_id', ''), item.get('number', 0))


def _normalize_channel_base(channel):
    number = channel.get('number', 0)
    try:
        number_int = int(number)
        if number_int <= 0:
            return None
    except Exception:
        return None

    stitched_urls = channel.get('stitched', {}).get('urls', []) or []
    stitched_url = stitched_urls[0].get('url') if stitched_urls else None
    if not stitched_url:
        return None

    name = channel.get('name') or '#{}'.format(number_int)
    thumb = channel.get('logo', {}).get('path', '')
    return {
        'number': number_int,
        'name': name,
        'display_name': name,
        'description': '',
        'thumb': thumb,
        'category': _extract_category(channel),
        'stitched_url': stitched_url,
        'slug': channel.get('slug', '') or '',
        'channel_id': channel.get('_id', '') or channel.get('id', '') or '',
    }


def _fetch_channels_raw(include_epg=False):
    cache_key = ('raw_static',)
    cached = _cache_get(cache_key)
    if cached is not None:
        return list(cached)

    now_utc, stop_utc = _window_for_fetch(False)
    from_str = now_utc.strftime('%Y-%m-%dT%H:%M:%SZ')
    to_str = stop_utc.strftime('%Y-%m-%dT%H:%M:%SZ')
    channels_url = 'https://api.pluto.tv/v2/channels?start={}&stop={}'.format(from_str, to_str)
    headers = {'User-Agent': USER_AGENT}
    response = SESSION.get(channels_url, headers=headers, timeout=15)
    response.raise_for_status()
    channels = response.json()
    if not isinstance(channels, list):
        channels = []
    normalized = []

    for channel in channels:
        item = _normalize_channel_base(channel)
        if item:
            normalized.append(item)

    normalized.sort(key=lambda item: item.get('number', 0))
    return _cache_set(cache_key, normalized, ttl=PLUTO_CHANNELS_TTL_SECONDS)


def _fetch_epg_overlay():
    cache_key = ('epg_overlay',)
    cached = _cache_get(cache_key)
    if cached is not None:
        return dict(cached)

    now_utc, stop_utc = _window_for_fetch(True)
    from_str = now_utc.strftime('%Y-%m-%dT%H:%M:%SZ')
    to_str = stop_utc.strftime('%Y-%m-%dT%H:%M:%SZ')
    channels_url = 'https://api.pluto.tv/v2/channels?start={}&stop={}'.format(from_str, to_str)
    headers = {'User-Agent': USER_AGENT}
    response = SESSION.get(channels_url, headers=headers, timeout=15)
    response.raise_for_status()
    channels = response.json()
    if not isinstance(channels, list):
        channels = []

    overlay = {}
    for channel in channels:
        item = _normalize_channel_base(channel)
        if not item:
            continue
        display_name, description = _extract_program_metadata(channel, now_utc, include_epg=True)
        overlay[_channel_key(item)] = {
            'display_name': display_name or item.get('name', ''),
            'description': description or ''
        }

    return _cache_set(cache_key, overlay, ttl=PLUTO_EPG_TTL_SECONDS)


def _apply_epg_overlay(channels, include_epg=True):
    base = [dict(item) for item in (channels or [])]
    if not include_epg:
        for item in base:
            item['display_name'] = item.get('name') or '#{}'.format(item.get('number', ''))
            item['description'] = ''
        return base

    try:
        overlay = _fetch_epg_overlay()
    except Exception:
        overlay = {}

    for item in base:
        default_name = item.get('name') or '#{}'.format(item.get('number', ''))
        meta = overlay.get(_channel_key(item), {})
        item['display_name'] = meta.get('display_name') or default_name
        item['description'] = meta.get('description') or ''
    return base


def get_channels(hide_no_logo=False, include_epg=True):
    cache_key = ('channels_static', bool(hide_no_logo))
    cached = _cache_get(cache_key)
    if cached is not None:
        channels = list(cached)
    else:
        channels = _fetch_channels_raw(include_epg=False)
        if hide_no_logo:
            channels = [item for item in channels if item.get('thumb')]
        channels = _cache_set(cache_key, list(channels), ttl=PLUTO_CHANNELS_TTL_SECONDS)
    return _apply_epg_overlay(channels, include_epg=include_epg)


def get_channels_grouped_by_category(hide_no_logo=False, include_epg=True):
    cache_key = ('grouped_static', bool(hide_no_logo))
    cached = _cache_get(cache_key)
    if cached is not None:
        return list(cached)
    grouped = {}
    for channel in get_channels(hide_no_logo=hide_no_logo, include_epg=False):
        category = channel.get('category') or 'Geral'
        grouped.setdefault(category, []).append(channel)
    result = sorted(grouped.items(), key=lambda item: item[0].lower())
    return _cache_set(cache_key, result, ttl=PLUTO_CHANNELS_TTL_SECONDS)


def get_channels_for_category(category, hide_no_logo=False, include_epg=True):
    wanted = (category or '').strip().lower()
    cache_key = ('category_static', wanted, bool(hide_no_logo))
    cached = _cache_get(cache_key)
    if cached is not None:
        result = list(cached)
    else:
        result = [
            item for item in get_channels(hide_no_logo=hide_no_logo, include_epg=False)
            if (item.get('category') or 'Geral').strip().lower() == wanted
        ]
        result = _cache_set(cache_key, result, ttl=PLUTO_CHANNELS_TTL_SECONDS)
    return _apply_epg_overlay(result, include_epg=include_epg)


def resolve_stream_url(slug='', channel_id='', stitched_url=''):
    session_id = get_session_id() or ensure_session('')
    cache_key = ('stream', session_id, slug or '', channel_id or '', stitched_url or '')
    cached = _cache_get(cache_key)
    if cached is not None:
        return cached

    resolved_stitched = stitched_url or ''
    if not resolved_stitched:
        for include_epg in (False,):
            for item in get_channels(hide_no_logo=False, include_epg=include_epg):
                if slug and item.get('slug') == slug:
                    resolved_stitched = item.get('stitched_url', '')
                    break
                if channel_id and item.get('channel_id') == channel_id:
                    resolved_stitched = item.get('stitched_url', '')
                    break
            if resolved_stitched:
                break

    session_token, stitcher_params = _fetch_boot_data()
    stream_url = _build_stream_url_from_stitched(resolved_stitched, session_token, stitcher_params)
    if not stream_url:
        return None
    return _cache_set(cache_key, stream_url, ttl=PLUTO_BOOT_TTL_SECONDS)


def playlist_pluto():
    return [
        (
            item.get('display_name') or item.get('name') or 'Pluto TV',
            item.get('description', ''),
            item.get('thumb', ''),
            resolve_stream_url(item.get('slug', ''), item.get('channel_id', ''), item.get('stitched_url', ''))
        )
        for item in get_channels(hide_no_logo=False)
    ]
