#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# pylint: disable=W0613, C0116
# type: ignore[union-attr]


import logging
import vlc
import os
import re
import sys
import json
import queue
import psutil
import logging
import requests
import time
import threading
import tempfile
import socketio
import uuid
import subprocess
import tkinter as tk
from io import BytesIO
from wifi import Cell, Scheme
import sqlite3  as lite
from PIL import Image
from datetime import datetime
import urllib3

try:
    from PIL import ImageTk
    PIL_IMAGETK_AVAILABLE = True
except Exception:
    ImageTk = None
    PIL_IMAGETK_AVAILABLE = False

sio = socketio.Client(
    ssl_verify=False,
    reconnection=True,
    reconnection_attempts=0,
    reconnection_delay=2,
    reconnection_delay_max=15,
)
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)


class IdleScreenManager:
    def __init__(self):
        self.root = None
        self.thread = None
        self.command_queue = queue.Queue()
        self.started = False
        self.visible = False
        self.enabled = True
        self.clock_var = None
        self.subtitle_var = None
        self.footer_var = None
        self.cards_frame = None
        self.empty_var = None
        self.empty_label = None
        self.logo_refs = []

    def start(self):
        if self.started or not self.enabled or not idle_screen_supported():
            return
        self.thread = threading.Thread(target=self._run, daemon=True)
        self.thread.start()
        self.started = True

    def show(self):
        if not idle_screen_supported():
            return
        self.start()
        self.command_queue.put(("show", None))

    def hide(self):
        if not self.started:
            return
        self.command_queue.put(("hide", None))

    def refresh(self):
        if not self.started:
            return
        self.command_queue.put(("refresh", None))

    def _run(self):
        try:
            self.root = tk.Tk()
            self.root.configure(bg="#08111d")
            self.root.title("IPTV Idle Screen")
            self.root.attributes("-fullscreen", True)
            self.root.attributes("-topmost", True)
            self.root.bind("<Escape>", lambda event: self.root.attributes("-fullscreen", False))

            header = tk.Frame(self.root, bg="#08111d")
            header.pack(fill="x", padx=60, pady=(45, 20))

            title = tk.Label(
                header,
                text="Latest Channels",
                fg="#f7fbff",
                bg="#08111d",
                font=("Helvetica", 34, "bold"),
                anchor="w",
            )
            title.pack(fill="x")

            self.subtitle_var = tk.StringVar(value="No channel running")
            subtitle = tk.Label(
                header,
                textvariable=self.subtitle_var,
                fg="#9ab4c8",
                bg="#08111d",
                font=("Helvetica", 18),
                anchor="w",
            )
            subtitle.pack(fill="x", pady=(8, 0))

            self.clock_var = tk.StringVar(value="")
            clock_label = tk.Label(
                header,
                textvariable=self.clock_var,
                fg="#6ee7b7",
                bg="#08111d",
                font=("Helvetica", 18, "bold"),
                anchor="e",
            )
            clock_label.pack(fill="x", pady=(12, 0))

            recent_frame = tk.Frame(self.root, bg="#0f1b2d", highlightbackground="#16324b", highlightthickness=1)
            recent_frame.pack(fill="both", expand=True, padx=60, pady=(10, 20))

            self.empty_var = tk.StringVar(value="Loading recent channels...")
            self.empty_label = tk.Label(
                recent_frame,
                textvariable=self.empty_var,
                fg="#f7fbff",
                bg="#0f1b2d",
                font=("Helvetica", 20),
                justify="center",
                anchor="center",
                padx=28,
                pady=28,
            )
            self.empty_label.pack(fill="both", expand=True)

            self.cards_frame = tk.Frame(recent_frame, bg="#0f1b2d")
            self.cards_frame.pack(fill="both", expand=True, padx=18, pady=18)
            for column_index in range(IDLE_SCREEN_CARD_COLUMNS):
                self.cards_frame.grid_columnconfigure(column_index, weight=1, uniform="idle_cards")

            self.footer_var = tk.StringVar(value="Waiting for a channel selection")
            footer = tk.Label(
                self.root,
                textvariable=self.footer_var,
                fg="#7b95aa",
                bg="#08111d",
                font=("Helvetica", 14),
                anchor="w",
            )
            footer.pack(fill="x", padx=60, pady=(0, 30))

            self.root.withdraw()
            self.root.after(250, self._process_commands)
            self.root.after(1000, self._tick_clock)
            self.root.after(IDLE_SCREEN_REFRESH_INTERVAL_MS, self._refresh_recent_data)
            self.root.mainloop()
        except Exception as exc:
            print("Unable to start idle screen:", exc, flush=True)
            self.enabled = False

    def _process_commands(self):
        if self.root is None:
            return

        while True:
            try:
                action, payload = self.command_queue.get_nowait()
            except queue.Empty:
                break

            if action == "show":
                self.visible = True
                self._render_recent_channels()
                self.root.deiconify()
                self.root.attributes("-fullscreen", True)
                self.root.attributes("-topmost", True)
                self.root.lift()
            elif action == "hide":
                self.visible = False
                self.root.withdraw()
            elif action == "refresh":
                self._render_recent_channels()

        self.root.after(250, self._process_commands)

    def _tick_clock(self):
        if self.root is None:
            return

        self.clock_var.set(datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
        self.root.after(1000, self._tick_clock)

    def _refresh_recent_data(self):
        if self.root is None:
            return

        if self.visible:
            sync_recent_channels_from_server()
            self._render_recent_channels()

        self.root.after(IDLE_SCREEN_REFRESH_INTERVAL_MS, self._refresh_recent_data)

    def _render_recent_channels(self):
        items = recent[:MAX_RECENT_CHANNELS]
        if self.cards_frame is None or self.empty_label is None:
            return

        self.logo_refs = []
        for child in self.cards_frame.winfo_children():
            child.destroy()

        if not items:
            self.empty_var.set("No recent channel history available.")
            self.empty_label.lift()
            self.footer_var.set("The screen will update when the server sends history or a channel is played locally.")
            return

        self.empty_label.lower()
        for index, item in enumerate(items, start=1):
            card = tk.Frame(
                self.cards_frame,
                bg="#13253a",
                highlightbackground="#21425f",
                highlightthickness=1,
                padx=18,
                pady=18,
            )
            row_index = (index - 1) // IDLE_SCREEN_CARD_COLUMNS
            column_index = (index - 1) % IDLE_SCREEN_CARD_COLUMNS
            card.grid(row=row_index, column=column_index, sticky="nsew", padx=12, pady=12)

            logo_image, logo_status = fetch_channel_logo_photo(item)
            logo_label = tk.Label(card, bg="#13253a")
            logo_label.pack(pady=(0, 16))
            if logo_image is not None:
                logo_label.configure(image=logo_image)
                self.logo_refs.append(logo_image)
            else:
                logo_label.configure(
                    text="No Logo",
                    fg="#9ab4c8",
                    font=("Helvetica", 16, "bold"),
                )

            name = item.get("name_channel") or item.get("name") or "Unknown channel"
            name_label = tk.Label(
                card,
                text=name,
                fg="#f7fbff",
                bg="#13253a",
                font=("Helvetica", 18, "bold"),
                wraplength=240,
                justify="center",
            )
            name_label.pack(fill="x")

            last_played_at = format_recent_timestamp(item.get("last_played_at"))
            meta_label = tk.Label(
                card,
                text=f"Last played: {last_played_at}",
                fg="#89a5ba",
                bg="#13253a",
                font=("Helvetica", 13),
                justify="center",
            )
            meta_label.pack(fill="x", pady=(10, 0))

        self.footer_var.set(f"Showing {len(items)} recent channels cached locally and refreshed from the server")


def idle_screen_supported():
    if sys.platform.startswith("linux"):
        return bool(os.environ.get("DISPLAY", "").strip())
    return True


def format_recent_timestamp(timestamp_value):
    if not timestamp_value:
        return "-"

    normalized_value = str(timestamp_value).replace("Z", "+00:00")
    for parser in (datetime.fromisoformat,):
        try:
            return parser(normalized_value).strftime("%Y-%m-%d %H:%M")
        except ValueError:
            pass

    for pattern in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%dT%H:%M:%S"):
        try:
            return datetime.strptime(str(timestamp_value), pattern).strftime("%Y-%m-%d %H:%M")
        except ValueError:
            pass

    return str(timestamp_value)


def build_logo_urls(item):
    id_channel = item.get("id_channel", "").strip()
    if not id_channel:
        return []

    return [url_update_server + f"logos/{id_channel}.jpg"]


def create_placeholder_logo():
    return Image.new("RGB", IDLE_SCREEN_LOGO_SIZE, color="#20364d")


def fetch_channel_logo_photo(item):
    if not server_connection_ready:
        return None, "Logo skipped: server connection not ready"

    if not PIL_IMAGETK_AVAILABLE:
        return None, "Logo skipped: PIL ImageTk unavailable"

    cache_key = f"{item.get('id_channel', '')}:{item.get('id_playlist', '')}"
    cached_logo = logo_cache.get(cache_key)
    if cached_logo is not None:
        return cached_logo, "Logo loaded from cache"

    image_obj = None
    last_error = "Logo fetch failed"
    for logo_url in build_logo_urls(item):
        try:
            response = requests.get(logo_url, timeout=LOGO_REQUEST_TIMEOUT_SECONDS, verify=False)
            if not response.ok:
                last_error = f"HTTP error {response.status_code}"
                continue

            image_obj = Image.open(BytesIO(response.content)).convert("RGB")
            last_error = f"HTTP {response.status_code}, {len(response.content)} bytes"
            break
        except Exception as exc:
            last_error = f"Request/decode error: {exc}"
            image_obj = None

    if image_obj is None:
        return None, last_error

    image_obj.thumbnail(IDLE_SCREEN_LOGO_SIZE, IMAGE_RESAMPLING_LANCZOS)
    background = Image.new("RGB", IDLE_SCREEN_LOGO_SIZE, color="#0c1827")
    offset_x = max((IDLE_SCREEN_LOGO_SIZE[0] - image_obj.width) // 2, 0)
    offset_y = max((IDLE_SCREEN_LOGO_SIZE[1] - image_obj.height) // 2, 0)
    background.paste(image_obj, (offset_x, offset_y))

    try:
        photo_image = ImageTk.PhotoImage(background)
    except Exception as exc:
        print("Unable to create Tk logo image:", exc, flush=True)
        return None, f"Tk image creation failed: {exc}"

    logo_cache[cache_key] = photo_image
    return photo_image, last_error


def normalize_recent_channel(item):
    return {
        "id_channel": str(item.get("id_channel") or item.get("_id") or ""),
        "id_playlist": str(item.get("id_playlist") or ""),
        "name_channel": str(item.get("name_channel") or item.get("name") or "Unknown channel"),
        "url_channel": str(item.get("url_channel") or item.get("url") or ""),
        "last_played_at": str(item.get("last_played_at") or item.get("datetime") or datetime.now().isoformat(timespec="seconds")),
    }


def load_recent_channels_from_db():
    global recent

    with db_lock:
        cur_ = con.cursor()
        cur_.execute(
            """
            SELECT id_channel, id_playlist, name_channel, url_channel, last_played_at
            FROM recent_channels_cache
            ORDER BY position ASC
            """
        )
        rows = cur_.fetchall()

    recent = [
        {
            "id_channel": str(row[0] or ""),
            "id_playlist": str(row[1] or ""),
            "name_channel": str(row[2] or ""),
            "url_channel": str(row[3] or ""),
            "last_played_at": str(row[4] or ""),
        }
        for row in rows
    ]
    return recent


def save_recent_channels_to_db(items):
    global recent

    normalized_items = [normalize_recent_channel(item) for item in items[:MAX_RECENT_CHANNELS]]

    with db_lock:
        cur_ = con.cursor()
        cur_.execute("DELETE FROM recent_channels_cache")
        for position, item in enumerate(normalized_items):
            cur_.execute(
                """
                INSERT INTO recent_channels_cache (
                    position, id_channel, id_playlist, name_channel, url_channel, last_played_at
                ) VALUES (?, ?, ?, ?, ?, ?)
                """,
                (
                    position,
                    item["id_channel"],
                    item["id_playlist"],
                    item["name_channel"],
                    item["url_channel"],
                    item["last_played_at"],
                ),
            )
        con.commit()

    recent = normalized_items
    idle_screen_manager.refresh()
    return normalized_items


def prepend_recent_channel(id_channel, url_channel, name_channel, id_playlist):
    current_items = load_recent_channels_from_db()
    new_item = normalize_recent_channel(
        {
            "id_channel": id_channel,
            "id_playlist": id_playlist,
            "name_channel": name_channel,
            "url_channel": url_channel,
            "last_played_at": datetime.now().isoformat(timespec="seconds"),
        }
    )

    filtered_items = [
        item for item in current_items
        if not (
            item.get("id_channel") == new_item["id_channel"]
            and item.get("url_channel") == new_item["url_channel"]
        )
    ]
    save_recent_channels_to_db([new_item] + filtered_items)


def parse_server_json_payload(response):
    payload = response.json()
    if isinstance(payload, str):
        payload = json.loads(payload)
    return payload


def sync_recent_channels_from_server():
    if not server_connection_ready:
        return recent

    try:
        response = requests.get(
            url_update_server + "recents",
            headers={"Cache-Control": "no-cache"},
            timeout=10,
            verify=False,
        )
        response.raise_for_status()
        payload = parse_server_json_payload(response)
        if not isinstance(payload, list):
            return recent

        normalized_items = [normalize_recent_channel(item) for item in payload[:MAX_RECENT_CHANNELS]]
        if normalized_items:
            return save_recent_channels_to_db(normalized_items)
    except Exception as exc:
        print("Unable to refresh recent channels from server:", exc, flush=True)

    return recent


def focus_vlc_player():
    global media_principal

    idle_screen_manager.hide()
    try:
        media_principal.set_fullscreen(True)
    except Exception as exc:
        print("Unable to focus VLC player:", exc, flush=True)


def wait_for_vlc_playback_start():
    global media_principal

    deadline = time.time() + VLC_PLAYBACK_START_TIMEOUT_SECONDS
    while time.time() < deadline:
        try:
            if media_principal.is_playing():
                focus_vlc_player()
                return True
        except Exception as exc:
            print("Unable to determine VLC playback state:", exc, flush=True)
            break
        time.sleep(VLC_PLAYBACK_POLL_INTERVAL_SECONDS)

    print("VLC did not report playback start before timeout", flush=True)
    return False


def start_playback_transition():
    threading.Thread(target=wait_for_vlc_playback_start, daemon=True).start()


def show_idle_screen():
    if mediaRunning == '':
        idle_screen_manager.show()


def hide_idle_screen():
    idle_screen_manager.hide()


def pulse_audio_available():
    pulse_server = os.environ.get("PULSE_SERVER", "").strip()
    if pulse_server:
        return True

    runtime_dir = os.environ.get("XDG_RUNTIME_DIR", "").strip()
    pulse_candidates = []
    if runtime_dir:
        pulse_candidates.append(os.path.join(runtime_dir, "pulse", "native"))

    pulse_candidates.append(f"/run/user/{os.getuid()}/pulse/native")
    return any(os.path.exists(candidate) for candidate in pulse_candidates)


def ensure_runtime_environment():
    runtime_dir = os.environ.get("XDG_RUNTIME_DIR", "").strip()
    if not runtime_dir:
        fallback_runtime = f"/tmp/iptv-runtime-{os.getuid()}"
        os.makedirs(fallback_runtime, exist_ok=True)
        os.environ["XDG_RUNTIME_DIR"] = fallback_runtime


def default_vlc_vout():
    forced_vout = os.environ.get("IPTV_VLC_VOUT", "").strip()
    if forced_vout:
        return forced_vout

    platform = sys.platform.lower()
    if platform.startswith("linux"):
        if os.environ.get("DISPLAY", "").strip():
            return "xcb_x11"
        return "fb"

    return ""

def build_vlc_instance():
    vlc_options = [
        "--intf=dummy",
        "--fullscreen",
        "--no-osd",
        "--no-video-title-show",
        "--no-snapshot-preview",

        # network/live buffering
        "--network-caching=3000",
        "--live-caching=3000",
        "--file-caching=1000",

        # late-frame handling
        "--drop-late-frames",
        "--skip-frames",

        # reduce timing pressure on live streams
        "--clock-jitter=0",
        "--clock-synchro=0",

        # troubleshooting
        "--verbose=2",
    ]

    forced_aout = os.environ.get("IPTV_VLC_AOUT", "").strip()
    if forced_aout:
        vlc_options.append(f"--aout={forced_aout}")
    elif not pulse_audio_available():
        vlc_options.append("--aout=alsa")

    forced_vout = os.environ.get("IPTV_VLC_VOUT", "").strip()
    if forced_vout:
        vlc_options.append(f"--vout={forced_vout}")

    return vlc.Instance(*vlc_options)


def new_media_player(url):
    player = vlc_instance.media_player_new()
    media = vlc_instance.media_new(url)

    media.add_option(":network-caching=3000")
    media.add_option(":live-caching=3000")
    media.add_option(":drop-late-frames")
    media.add_option(":skip-frames")
    media.add_option(":clock-jitter=0")
    media.add_option(":clock-synchro=0")

    forced_vout = os.environ.get("IPTV_VLC_VOUT", "").strip()
    if forced_vout:
        media.add_option(f":vout={forced_vout}")

    player.set_media(media)
    return player


def get_snapshot_filepath():
    return os.path.join(tempfile.gettempdir(), f"iptv_snapshot_{IdDevice}.jpg")


def upload_snapshot_file(snapshot_path):
    if not os.path.exists(snapshot_path):
        return False

    with open(snapshot_path, "rb") as snapshot_file:
        response = requests.post(
            url_update_server + 'upload_snapshot',
            data={'id_device': IdDevice},
            files={'file': (os.path.basename(snapshot_path), snapshot_file, 'image/jpeg')},
            timeout=10,
            verify=False
        )
    response.raise_for_status()
    return True


def capture_and_upload_snapshot():
    global media_principal
    global data_running

    if not data_running:
        return False

    snapshot_path = get_snapshot_filepath()
    snapshot_status = media_principal.video_take_snapshot(0, snapshot_path, 0, 0)
    if snapshot_status != 0:
        print('snapshot capture failed', flush=True)
        return False

    upload_snapshot_file(snapshot_path)
    return True


def snapshot_loop():
    while True:
        try:
            if auto_snapshot_enabled and data_running:
                capture_and_upload_snapshot()
        except Exception as exc:
            print('snapshot loop error', exc, flush=True)
        time.sleep(SNAPSHOT_INTERVAL_SECONDS)

def RepresentsInt(s):
    try:
        int(s)
        return True
    except ValueError:
        return False

def convertMillis(millis):
     seconds=(millis/1000)%60
     minutes=(millis/(1000*60))%60
     hours=(millis/(1000*60*60))%24
     print(str(seconds)+" "+str(minutes)+" "+ str(hours))
     return seconds, minutes, hours

def is_vpn_connected():
    try:
        output = subprocess.check_output("ip a | grep tun0", shell=True)
        return "tun0" in output.decode()
    except subprocess.CalledProcessError:
        return False

def connect_vpn():
    global vpn_process

    if is_vpn_connected():
        return "VPN already connected"

    subprocess.call("sudo pkill openvpn", shell=True)
    subprocess.call("sudo ip link delete tun0", shell=True)

    time.sleep(1)
    vpn_process = subprocess.Popen(
        ["sudo", "openvpn", "--config", vpn_config, "--auth-user-pass", auth_file],
        stdout=subprocess.PIPE,
        stderr=subprocess.STDOUT,
        text=True
    )

    # Wait for connection confirmation
    for _ in range(30):
        if is_vpn_connected():
            return "VPN connected"
        time.sleep(1)

    return "VPN connection failed"

def disconnect_vpn():
    global vpn_process
    subprocess.call("sudo pkill openvpn", shell=True)
    subprocess.call("sudo ip link delete tun0", shell=True)
    vpn_process = None
    return "VPN disconnected"

def findInfo(tag, s):
    end = '"'
    if (s.find(tag)>-1):
        t = s[s.find(tag)+len(tag)+2:s.rfind(end)]
        return  t[0:t.find(end)]
    else:
        return ""


def restart_process():
    try:
        print('restart new process')
        p = psutil.Process(os.getpid())
        for handler in p.get_open_files() + p.connections():
            os.close(handler.fd)
    except Exception as e:
        logging.error(e)

    python = sys.executable
    os.execl(python, python, *sys.argv)


def restart_main_iptv_process():
    python = sys.executable
    main_script = os.path.join(path, 'IPTV.py')
    os.execl(python, python, main_script)


def stop():
    global media_principal
    global mediaRunning 
    global data_running
    data_running = {}
    mediaRunning = ''
    media_principal.stop()
    sync_recent_channels_from_server()
    show_idle_screen()
    

def display_overlay_message(message, duration_ms=10000):
    global media_principal

    try:
        media_principal.video_set_marquee_int(vlc.VideoMarqueeOption.Enable, 1)
        media_principal.video_set_marquee_string(vlc.VideoMarqueeOption.Text, message)
        media_principal.video_set_marquee_int(vlc.VideoMarqueeOption.Size, 42)
        media_principal.video_set_marquee_int(vlc.VideoMarqueeOption.Timeout, duration_ms)
        media_principal.video_set_marquee_int(vlc.VideoMarqueeOption.Position, 8)
    except Exception as exc:
        print('Unable to display overlay message:', exc, flush=True)


def playMedia(id_channel, url_channel, name_channel, id_playlist):
    global media_principal
    global mediaRunning
    global recent
    global data_running

    try:
        media_principal.stop()
        media_principal.release()
    except Exception as e:
        print("Unable to reset VLC player:", e, flush=True)

    media_principal = new_media_player(url_channel)
    try:
        media_principal.set_fullscreen(True)
    except Exception as e:
        print("Unable to enable fullscreen:", e, flush=True)
    media_principal.audio_set_volume(200)
    media_principal.play()
    start_playback_transition()
    mediaRunning = name_channel
    data_running = {'name_channel':name_channel, 'id_channel':id_channel, 'id_playlist':id_playlist, 'url_channel': url_channel, 'progress':0}
    prepend_recent_channel(id_channel, url_channel, name_channel, id_playlist)
    print(' DATA : ', data_running, flush=True)

    media_principal.video_set_marquee_int(vlc.VideoMarqueeOption.Enable, 1)
    media_principal.video_set_marquee_string(vlc.VideoMarqueeOption.Text, name_channel)
    media_principal.video_set_marquee_int(vlc.VideoMarqueeOption.Size, 48)  # Font size
    media_principal.video_set_marquee_int(vlc.VideoMarqueeOption.Timeout, 7000)  # Duration in milliseconds
    media_principal.video_set_marquee_int(vlc.VideoMarqueeOption.Position, 8)  # Position on screen


def update_source():
    global path
    r = requests.get(url_update_server+'get_source', headers={'Cache-Control': 'no-cache'}, verify=False)
    r.raise_for_status()
    temp_path = path + 'IPTV.py.tmp'
    try:
        with open(temp_path, 'wb') as updated_file:
            updated_file.write(r.content)
        os.replace(temp_path, path + 'IPTV.py')
    except Exception as exc:
        print('error update_source', exc, flush=True)
        if os.path.exists(temp_path):
            os.remove(temp_path)
        raise


def wait_for_internet_connection():
    while True:
        print("Checking connexion ...", flush=True)
        for check_url, verify_ssl in CONNECTIVITY_CHECK_TARGETS:
            try:
                requests.get(check_url, timeout=CONNECTIVITY_CHECK_TIMEOUT_SECONDS, verify=verify_ssl)
                print(f"Connectivity check succeeded with {check_url}", flush=True)
                return
            except requests.RequestException as exc:
                print(f"Connectivity check failed for {check_url}: {exc}", flush=True)

        time.sleep(CONNECTIVITY_CHECK_RETRY_SECONDS)


def ensure_socket_connection(force_reset=False):
    global last_connect_attempt_ts
    global server_connection_ready

    with socket_connect_lock:
        if sio.connected and not force_reset:
            server_connection_ready = True
            return True

        now = time.time()
        if not force_reset and (now - last_connect_attempt_ts) < SOCKET_RETRY_MIN_INTERVAL:
            return sio.connected

        last_connect_attempt_ts = now

        try:
            if force_reset:
                try:
                    sio.disconnect()
                except Exception:
                    pass

            wait_for_internet_connection()
            print('Attempting socket connection...', flush=True)
            sio.connect(
                url_update_server,
                wait=True,
                wait_timeout=SOCKET_CONNECT_TIMEOUT,
                transports=['websocket', 'polling']
            )
            print('Socket connection ready', flush=True)
            server_connection_ready = True
            return True
        except Exception as exc:
            print('Socket reconnection failed', exc, flush=True)
            server_connection_ready = False
            return False


def polling_server():
    global media_principal
    global data_running
    global status_vpn
    print('polling_server')
    while True:
        try:
            if not sio.connected:
                ensure_socket_connection()
                time.sleep(POLLING_INTERVAL_SECONDS)
                continue

            #r = requests.get(url_update_server+'ping?name=hirondelle&', allow_redirects=True)
            data_running_tmp = data_running
            milli = media_principal.get_time()
            percentage = media_principal.get_position()*100
            
            if data_running:
                print(data_running_tmp)
                data_running = {'name_channel':data_running_tmp['name_channel'], 'id_channel':data_running_tmp['id_channel'], 'id_playlist':data_running_tmp['id_playlist'], 'url_channel':data_running_tmp['url_channel'], 'progress':milli, 'percentage':percentage}
            sio.emit('polling', {
                'name': name_device,
                'IdDevice': IdDevice,
                'mediaRunning': mediaRunning,
                'vpn_status': is_vpn_connected(),
                'data_running': data_running,
                'auto_snapshot_enabled': auto_snapshot_enabled,
                'snapshot_interval_seconds': SNAPSHOT_INTERVAL_SECONDS
            })
        except Exception as exc:
            print('Polling failed, will reconnect', exc, flush=True)
            ensure_socket_connection(force_reset=True)
        time.sleep(POLLING_INTERVAL_SECONDS)




def sendMessage(fromSidDevice, toSidDevice, typeCommand, payload, num_packet):
    new_data = {'fromSidDevice': fromSidDevice, 'toSidDevice': toSidDevice, 'typeCommand': typeCommand, 'payload': payload, 'packet':num_packet}
    print(new_data)
    if sio.connected or ensure_socket_connection():
        sio.emit('communication', new_data)
    else:
        print('Socket disconnected, unable to send message', flush=True)


@sio.event
def connect():
    global server_connection_ready
    server_connection_ready = True
    print('connection established')


@sio.event
def connect_error(data):
    print('connection error', data, flush=True)


@sio.event
def communication(data):

    toSidDevice = data['toSidDevice']
    fromSidDevice = data['fromSidDevice']
    typeCommand = data['typeCommand']

    global recent
    global media_principal
    global mediaRunning
    global data_running
                
    # --------------------------------
    # OFF
    # --------------------------------
    if typeCommand == "off":
        stop()
        sendMessage(toSidDevice, fromSidDevice, 'warning', 'Off', 0)
        sendMessage(toSidDevice, fromSidDevice, 'playing', 'no channel', 0)

    # --------------------------------
    # VPN MANAGEMENT
    # --------------------------------  
    if typeCommand == "vpn":
        command_vpn = data['payload']
        if command_vpn == "connect":
            sendMessage(toSidDevice, fromSidDevice, 'warning', 'Start VPN Connexion', 0)
            result = connect_vpn()
        elif command_vpn == "status":
            result = "connected" if is_vpn_connected() else "disconnected"
        elif command_vpn == "disconnect":
            sendMessage(toSidDevice, fromSidDevice, 'warning', 'Start VPN Disconnexion', 0)
            result = disconnect_vpn()
        else:
            result = "Unknown command"
        sendMessage(toSidDevice, fromSidDevice, 'warning', result, 0)



    # --------------------------------
    # PLAY A CHANNEL ON
    # --------------------------------
    if typeCommand == "on":
        print ("switch on")
        data_channel = data['payload']
        name_channel = data_channel['name'] 
        url_channel = data_channel['url']
        id_channel = data_channel['id_channel']
        id_playlist = data_channel['id_playlist']

        print(id_channel, name_channel, url_channel, flush=True)
        playMedia(id_channel, url_channel, name_channel, id_playlist)
        sendMessage(toSidDevice, fromSidDevice, 'playing', name_channel, 0)
        sendMessage(toSidDevice, fromSidDevice, 'warning', 'Starting media', 0)
    

    # --------------------------------
    # GET IP ADDRESS
    # --------------------------------
    if typeCommand == "getIP":
        my_IP = requests.get("https://api.ipify.org").text
        sendMessage(toSidDevice, fromSidDevice, 'warning', 'IP : ' + my_IP, 0)

    # --------------------------------
    # GET CHANNEL RUNNING
    # --------------------------------
    if typeCommand == "getChannelRunning":
        new_data ={}
        if mediaRunning == '':
            mr = 'no channel'
            percentage = 0
            milli = 0
            id_channel = ''
        else:
            mr = mediaRunning
            percentage = media_principal.get_position()*100
            milli = media_principal.get_time()
            id_channel = data_running['id_channel']

        new_data = {'name': mr, 'percentage':percentage, 'millisec':milli, 'id_channel': id_channel}
        sendMessage(toSidDevice, fromSidDevice, 'playing', new_data, 0)

    # --------------------------------
    # SET SUBTITLE
    # --------------------------------
    if typeCommand == "subtitle":
        track = data['payload']
        media_principal.video_set_spu(track)
        sendMessage(data['toSidDevice'], data['fromSidDevice'], 'warning', 'Track Num ' + str(track), 0)

    # --------------------------------
    # RESUME
    # --------------------------------
    if typeCommand == "resume":
        media_principal.set_pause(0)
        sendMessage(toSidDevice, fromSidDevice, 'warning', 'Resume done', 0)

    # --------------------------------
    # PAUSE
    # --------------------------------
    if typeCommand == "pause":
        media_principal.set_pause(1)
        sendMessage(toSidDevice, fromSidDevice, 'warning', 'Pause done', 0)

    # --------------------------------
    # GET SOFTWARE VERSION 
    # --------------------------------
    if typeCommand == "version":
        version_message = 'Version ' + str(num_version)
        sendMessage(toSidDevice, fromSidDevice, 'warning', version_message, 0)
        display_overlay_message(version_message, 10000)

    # --------------------------------
    # AUTO SNAPSHOT TOGGLE
    # --------------------------------
    if typeCommand == "toggleAutoSnapshot":
        global auto_snapshot_enabled
        auto_snapshot_enabled = bool(data['payload'])
        state_label = 'enabled' if auto_snapshot_enabled else 'disabled'
        sendMessage(toSidDevice, fromSidDevice, 'warning', 'Auto snapshot ' + state_label, 0)

    # --------------------------------
    # SNAPSHOT INTERVAL
    # --------------------------------
    if typeCommand == "setSnapshotInterval":
        global SNAPSHOT_INTERVAL_SECONDS
        try:
            interval_seconds = int(data['payload'])
        except (TypeError, ValueError):
            sendMessage(toSidDevice, fromSidDevice, 'warning', 'Invalid snapshot interval', 0)
        else:
            if interval_seconds < 2:
                sendMessage(toSidDevice, fromSidDevice, 'warning', 'Snapshot interval must be at least 2 seconds', 0)
            else:
                SNAPSHOT_INTERVAL_SECONDS = interval_seconds
                sendMessage(
                    toSidDevice,
                    fromSidDevice,
                    'warning',
                    'Snapshot interval set to ' + str(SNAPSHOT_INTERVAL_SECONDS) + ' seconds',
                    0
                )

    # --------------------------------
    # MOVETO
    # --------------------------------
    if typeCommand == "moveto":
        percentage = data['payload']
        new_position_value = float(percentage) / 100 
        media_principal.set_position(new_position_value)
        sendMessage(toSidDevice, fromSidDevice, 'warning', 'Moved to ' + str(percentage), 0)

    # --------------------------------
    # UPDATE SOFTWARE
    # --------------------------------
    if typeCommand == "update_source":
        sendMessage(toSidDevice, fromSidDevice, 'warning', 'Updating source and restarting client', 0)
        time.sleep(1)
        update_source()
        restart_main_iptv_process()

    # --------------------------------
    # REBOOT
    # --------------------------------
    if typeCommand == "reboot":
        os.system('sudo reboot')
        sendMessage(toSidDevice, fromSidDevice, 'warning', 'start rebooting ...', 0)

 
    # --------------------------------
    # CHANGE DEVICE NAME AND REBUILD CONFIG FILE
    # --------------------------------
    if typeCommand == "changeDeviceName":
        global name_device
        name_device = data['payload']
        print(name_device)
        query = "UPDATE settings SET valueParameter = '" + name_device + "' WHERE nameParameter = 'deviceName'"
        cur_ = con.cursor()
        cur_.execute(query)
        con.commit()
        sendMessage(toSidDevice, fromSidDevice, 'warning', 'Configuration saved', 0)

    # --------------------------------
    # GET LIST OF WIFI NETWORKS
    # --------------------------------
    if typeCommand == "listSSID":
        sendMessage(toSidDevice, fromSidDevice, 'warning', 'Retreiving Wifi data', 0)
        cells = Cell.all('wlan0')    
        resWifi = []
        for c in cells:
            item = []
            item.append(c.ssid)
            item.append(str(c.signal))
            item.append(str(c.quality)) 
            item.append(str(c.encrypted))
            resWifi.append(item)
        sendMessage(toSidDevice, fromSidDevice, 'wifiupdate', resWifi, 0)
        sendMessage(toSidDevice, fromSidDevice, 'warning', 'Wifi data received', 0)

    # --------------------------------
    # TAKE SNAPSHOT
    # --------------------------------
    if typeCommand == "takeSnapshot":
        try:
            print('take snapshot')
            time.sleep(1)
            capture_and_upload_snapshot()
        except Exception as e:
            sendMessage(toSidDevice, fromSidDevice, 'warning', e, 0)

    # --------------------------------
    # GET NUMBER OF CHANNELS
    # --------------------------------
    if typeCommand == "numberOfChannels":
        numberOfChannels = len(channels)
        sendMessage(toSidDevice, fromSidDevice, 'warning', 'Number of channels : ' + str(numberOfChannels), 0)


    # --------------------------------
    # Command line
    # --------------------------------
    if typeCommand == "command_line":
        records = re.split(r' ', data['payload'])
        #result = subprocess.run(records, capture_output=True, text=True)
        process = subprocess.Popen(records, stdout=subprocess.PIPE, text=True)
        for line in process.stdout:
            print(line, end="")
            sendMessage(toSidDevice, fromSidDevice, 'display_command_result', line, 0)


        #print(result.stdout)
        

@sio.event
def disconnect():
    global server_connection_ready
    server_connection_ready = False
    print('Disconnected from server', flush=True)



# --------------------------------
# INITIALIZATION DATABASE 
# --------------------------------
def initDB():
    global con
    try:
        con = lite.connect(path+db_file, check_same_thread=False)
        cur = con.cursor()
        cur.execute('SELECT SQLITE_VERSION()')
        data = cur.fetchone()
        print ("Connexion succeed : SQLite version: %s" % data)
        cur.execute(
            """
            CREATE TABLE IF NOT EXISTS recent_channels_cache (
                position INTEGER PRIMARY KEY,
                id_channel TEXT,
                id_playlist TEXT,
                name_channel TEXT NOT NULL,
                url_channel TEXT NOT NULL,
                last_played_at TEXT NOT NULL
            )
            """
        )
        con.commit()

    except (lite.Error, e):
        print ("Error %s:" % e.args[0])
        sys.exit(1)


def initIPTV():
    global token_telegram
    global name_device
    global channels
    global playlists
    global con
    global IdDevice
    global recent

    # GET ID UNIQUE DEVICE OR CREATE IT
    query = "SELECT valueParameter FROM settings WHERE nameParameter = 'IdDevice'"
    cur_ = con.cursor()
    cur_.execute(query)
    record = cur_.fetchone()
    if record:
        IdDevice = record[0]
        print(IdDevice)
    else:
        print("No ID device found")
        IdDevice = str(uuid.uuid4())
        query = "INSERT INTO settings (nameParameter, valueParameter) VALUES ('IdDevice', '"+IdDevice+"')"
        cur_ = con.cursor()
        cur_.execute(query)
        con.commit()


    # GET DEVICE NAME
    query = "SELECT valueParameter FROM settings WHERE nameParameter = 'deviceName'"
    cur_ = con.cursor()
    cur_.execute(query)
    valueParameter = cur_.fetchone()
    if valueParameter is not None:
        name_device = valueParameter
        print('Update device name : ' + str(name_device))


    # SAVE PID
    query = "DELETE FROM settings WHERE nameParameter = 'pid'"
    cur_ = con.cursor()
    cur_.execute(query)
    con.commit()
    pid = os.getpid()
    query = "INSERT INTO settings (nameParameter, valueParameter) VALUES ('pid', '"+str(pid)+"')"
    cur_ = con.cursor()
    cur_.execute(query)
    con.commit()



# ----------------------------------------------------------------
# ----------------------------------------------------------------
num_version='5.0.4'
# ----------------------------------------------------------------
# ----------------------------------------------------------------


# ----------------------------------------------------------------
# VARIABLES GLOBAL
# ----------------------------------------------------------------
url_update_server = 'https://iptv.mrk.ovh/'
#url_update_server = 'https://tv.mrk.ovh:5011/'
db_file = 'config.db'
path = os.path.dirname(os.path.realpath(__file__))+'/'
con = None
pid = os.getppid()
print('PID : ', pid)

channels = []
playlists = []
IdDevice = "0"
NUM_MAX_CHANNEL = 500000

recent = []
mediaRunning = ''
status_vpn = 'disconnected'
data_running = {}
auto_snapshot_enabled = True
SNAPSHOT_INTERVAL_SECONDS = 10
POLLING_INTERVAL_SECONDS = 10
SOCKET_CONNECT_TIMEOUT = 10
SOCKET_RETRY_MIN_INTERVAL = 3
CONNECTIVITY_CHECK_TIMEOUT_SECONDS = 3
CONNECTIVITY_CHECK_RETRY_SECONDS = 2
CONNECTIVITY_CHECK_TARGETS = [
    (url_update_server, False),
    ("https://www.google.com/generate_204", True),
    ("http://clients3.google.com/generate_204", False),
]
last_connect_attempt_ts = 0
server_connection_ready = False
socket_connect_lock = threading.Lock()
db_lock = threading.Lock()
MAX_RECENT_CHANNELS = 12
IDLE_SCREEN_REFRESH_INTERVAL_MS = 60000
idle_screen_manager = IdleScreenManager()
VLC_PLAYBACK_START_TIMEOUT_SECONDS = 8
VLC_PLAYBACK_POLL_INTERVAL_SECONDS = 0.2
IDLE_SCREEN_CARD_COLUMNS = 3
IDLE_SCREEN_LOGO_SIZE = (220, 124)
LOGO_REQUEST_TIMEOUT_SECONDS = 5
logo_cache = {}
IMAGE_RESAMPLING_LANCZOS = getattr(getattr(Image, "Resampling", Image), "LANCZOS")


ensure_runtime_environment()
vlc_instance = build_vlc_instance()
media_principal = new_media_player("http://mag.hi-ott.com:80/120643/NS3MV8YZTS/51648")

token_telegram = "token"
name_device = ''
vpn_process = None
vpn_config = "/home/pi/Documents/ca1629.nordvpn.com.tcp.ovpn"
auth_file = "/home/pi/Documents/auth.txt"


# ----------------------------------------------------------------
# ----------------------------------------------------------------

logging.basicConfig(
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO
)
logger = logging.getLogger(__name__)

# ----------------------------------------------------------------
# LOOP PRINCIPAL 
# ----------------------------------------------------------------
if __name__ == '__main__':

        initDB()
        initIPTV()

        wait_for_internet_connection()
        load_recent_channels_from_db()
        show_idle_screen()

        threading.Thread(target=snapshot_loop, daemon=True).start()
        ensure_socket_connection(force_reset=True)
        sync_recent_channels_from_server()
        idle_screen_manager.refresh()
        
        polling_server()
