
import re
import os  
import cv2
import time 
import glob
import shutil
import random
import logging
import requests 
from pathlib import Path 
from ffmpeg import FFmpeg
from datetime import datetime
from watchdog.observers import Observer
from moviepy.editor import VideoFileClip
from watchdog.events import FileSystemEventHandler

# Get environment variables
# Streamed Channel Name 
channel_name = os.environ.get("CHANNEL_NAME")
segment_duration = os.environ.get("SEGMENT_DURATION")
max_ts_file_number = os.environ.get("MAX_TS_FILE_NUMBER")
output_path = "/app/streams"
folder_to_watch = f"{output_path}/{channel_name}/hls"   
playlist_file_path = f"{output_path}/{channel_name}/hls/playlist.m3u8"
# Telegram bot token 
bot_token="6856325904:AAHSaLJKN8A8VxDGvUq120tJt6QTZWoz_s4"
# Telegram group chat ID
chat_id="-4183601434"  

# Create a logger
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
# Create a file handler
file_handler = logging.FileHandler(os.path.join(f"{output_path}/{channel_name}/logs", "2M_stream_listner.log"))
file_handler.setLevel(logging.INFO) 
# Create a formatter and set it for the handler
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
file_handler.setFormatter(formatter) 
# Add the file handler to the logger
logger.addHandler(file_handler)


# Function to send a message to the Telegram group
def send_telegram_message(bot_token, chat_id, message):
    """
    Sends a message to a Telegram group.

    Args:
        bot_token (str): The bot token obtained from BotFather.
        chat_id (str): The ID of the Telegram group or user.
        message (str): The message to be sent.

    Returns:
        bool: True if the message is sent successfully, False otherwise.
    """
    try:
        url = f"https://api.telegram.org/bot{bot_token}/sendMessage"
        payload = {
            "chat_id": chat_id,
            "text": message
        }
        response = requests.post(url, json=payload)
        if response.status_code == 200:
            print("Message sent successfully.")
            return True
        else:
            logger.info(f"Failed to send message. Status code: {response.status_code}")
            return False
    except Exception as e:
        logger.info(f"An error occurred: {e}")
        return False
    
# Function to create the output directory if it does not exist
def create_output_directory(output_dir, channel_name):
    # Construct the output path
    output_path = os.path.join(output_dir, channel_name, 'hls')
    # Create the directory if it doesn't exist
    if not os.path.isdir(output_path):
        os.makedirs(output_path, exist_ok=True)
        # logger.info(f"Created directory: {output_path}")
    # else:
    #     logger.info(f"Directory already exists: {output_path}")

# Function to wait until index.m3u8 file created
def wait_for_index_m3u8(output_dir, channel_name):
    index_m3u8_path = Path(output_dir).resolve() / channel_name / 'hls' / 'index.m3u8'
    while not index_m3u8_path.exists():
        time.sleep(0.2)

# Function to ensure playlist.m3u8 file exists
def ensure_playlist_m3u8_exists(playlist_file_path):   
    playlist_file_path = Path(playlist_file_path)
    if not playlist_file_path.is_file():
        playlist_content = [
            '#EXTM3U',
            '#EXT-X-VERSION:3',
            '#EXT-X-TARGETDURATION:6',
            '#EXT-X-MEDIA-SEQUENCE:0\n'
        ]
        with open(playlist_file_path, 'w') as f:
            f.write('\n'.join(playlist_content))
        logging.info(f"Created playlist file: {playlist_file_path}")
    else:
        logging.info(f"Playlist file already exists: {playlist_file_path}")

# Function to ensure master.m3u8 file exists with required text
def ensure_master_m3u8_exists(master_dir): 
    master_path = Path(master_dir) / "master.m3u8" 
    if not master_path.is_file():
        master_content = [
            '#EXTM3U',
            '#EXT-X-VERSION:3',
            '#EXT-X-INDEPENDENT-SEGMENTS',
            '#EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,GROUP-ID="CC",LANGUAGE="eng",NAME="english",DEFAULT=YES,AUTOSELECT=YES,INSTREAM-ID="CC1"',
            '#EXT-X-STREAM-INF:BANDWIDTH=3405600,RESOLUTION=1280x720,CODECS="avc1.4d401f,mp4a.40.2",FRAME-RATE=25,CLOSED-CAPTIONS="CC"',
            'playlist.m3u8'  
        ]
        with open(master_path, 'w') as f:
            f.write('\n'.join(master_content))
        logging.info(f"Created master file: {master_path}")
    else:
        logging.info(f"Master file already exists: {master_path}")

# 
def copy_file(source_path, destination_path):
    """Copy a file from source_path to destination_path."""
    try:
        shutil.copy(source_path, destination_path)
        print(f"File copied from {source_path} to {destination_path}")
    except Exception as e:
        print(f"Error copying file: {e}")
 

class JingleDetector:
    def __init__(self, jingles_folder, iframes_folder):
        self.jingles_folder = jingles_folder
        self.iframes_folder = iframes_folder
        self.jingle_paths = self.load_jingle()

    def create_folder(self, folder_path):
        """Creates a folder if it doesn't exist."""
        os.makedirs(folder_path, exist_ok=True)
        # print(f"Folder created: {folder_path}")

    def extract_iframes(self, video_path):
        try:
            self.create_folder(self.iframes_folder) 
            ffmpeg = (
                FFmpeg()
                .option("y")
                .input(video_path)
                .output(
                    os.path.join(self.iframes_folder, f"{os.path.splitext(os.path.basename(video_path))[0]}_%d.png"),
                    vf="select=gt(scene\\,0.10)",
                    vsync="vfr",
                    frame_pts="true"
                )
            )
            # Run FFmpeg command
            ffmpeg.execute() 
            # Get list of created frames 
            return glob.glob(os.path.join(self.iframes_folder, f"{os.path.splitext(os.path.basename(video_path))[0]}_*.png"))
        except Exception as e:
            logging.error(f"Error extracting iframes from {video_path}: {e}")
            return []

    def compare_images(self, image_1, image_2): 
        start_time = time.time()
        image_1=cv2.imread(image_1)
        image_2=cv2.imread(image_2)
        first_image_hist = cv2.calcHist([image_1], [0], None, [256], [0, 256])
        second_image_hist = cv2.calcHist([image_2], [0], None, [256], [0, 256])

        img_hist_diff = cv2.compareHist(first_image_hist, second_image_hist, cv2.HISTCMP_BHATTACHARYYA)
        img_template_probability_match = cv2.matchTemplate(first_image_hist, second_image_hist, cv2.TM_CCOEFF_NORMED)[0][0]
        img_template_diff = 1 - img_template_probability_match

        # taking only 10% of histogram diff, since it's less accurate than template method
        commutative_image_diff = (img_hist_diff * 0.50) + img_template_diff
        end_time = time.time()
        # print("Time taken to compare images in Seconds: {}".format(end_time - start_time))
        return commutative_image_diff

    def load_jingle(self):
        jingle_paths = []
        for jingle_name in os.listdir(self.jingles_folder):
            jingle_path = os.path.join(self.jingles_folder, jingle_name)
            if os.path.isdir(jingle_path):
                for image_name in os.listdir(jingle_path):
                    image_path = os.path.join(jingle_path, image_name)
                    jingle_paths.append((os.path.splitext(image_name)[0], image_path))
        return jingle_paths

    def remove_iframe(self, iframe_path):
        """Remove the iframe file."""
        try:
            os.remove(iframe_path)
            # logger.info(f"Removed iframe: {iframe_path}")
        except Exception as e:
            logger.info(f"Error removing iframe: {e}")
        
    def detect_jingle(self, ts_file):
        """
        Detects jingles in the given .ts file.
        
        Args:
            ts_file (str): The path to the .ts file to analyze.
            
        Returns:
            tuple or None: A tuple containing the jingle name, iframe path, and similarity score if a jingle is detected.
                Returns None if no jingle is detected.
        """
        # Extract IFrames from the .ts file
        iframes = self.extract_iframes(ts_file)
        # If no IFrames are extracted, return None
        if not iframes:
            # logger.error(f"Error: No IFrames extracted from the .ts file. [{ts_file}]")
            return None
        # Iterate over each extracted IFrame
        for iframe_path in iframes:
            # Log the current timestamp and iframe path
            # logger.info(f"{datetime.now()} {iframe_path}")
            # Flag to track if any jingle matches the frame
            found_matching_jingle = None 
            # Iterate over each jingle and its corresponding image path
            for jingle_name, jingle_image_path in self.jingle_paths:
                # Compare the jingle image with the current iframe
                similarity = self.compare_images(jingle_image_path, iframe_path)  
                # logger.info(f"[{datetime.now()}] :: [{jingle_image_path}] ** [{iframe_path}] --> [{similarity}]")
                # If similarity is below the threshold, consider it a jingle
                if similarity < 0.1:
                    found_matching_jingle = True
                    return jingle_name, iframe_path, similarity  
                # Set the flag if similarity is greater than the threshold
                if similarity > 0.1 :
                    found_matching_jingle = False 
            # If no jingle matches the frame, remove the frame
            if found_matching_jingle != None and found_matching_jingle == False:
                self.remove_iframe(iframe_path) 
        # Return None if no jingle is detected
        return None


class TsFilesHandler(FileSystemEventHandler): 
    def __init__(self, playlist_file, segment_limit, use_jingles):
        super(TsFilesHandler, self).__init__()
        self.playlist_file = playlist_file
        self.segment_limit = int(segment_limit)
        self.hls_media_sequence = 0
        self.use_jingles = use_jingles
        self.ad_break_start_time = None
        if self.use_jingles: 
            self.jingle_detector = JingleDetector(f"{output_path}/jingles", f"{output_path}/{channel_name}/iframes")

    def on_created(self, event):
        """
        Callback function triggered when a file is created in the watched directory.

        Args:
            event (FileSystemEvent): The event object representing the file creation event.
        """
        # Check if the event corresponds to a file creation and if it's a .ts file  
        if not event.is_directory and event.src_path.lower().endswith(".ts"):  
            try:
                # Check if jingle detection is enabled 
                jingle_detected = self.jingle_detector.detect_jingle(event.src_path) if self.use_jingles else None 
                # Check if jingle detected
                if jingle_detected : 
                    # detection datetime
                    start_datetime = datetime.now()
                    # Get Iframe Detected Information
                    jingle_name, iframe_path, similarity = jingle_detected
                    # Construct iframe URL and path for logging
                    iframe_url = iframe_path.replace("/app/streams/2M_Monde_HLS/iframes/", "http://173.212.199.208/demo-sect35/streams/2M_Monde_HLS/iframes/")
                    iframe_path = iframe_path.replace("/app/streams/2M_Monde_HLS/iframes/", "/var/www/html/demo-sect35/streams/iframes/")
                    # Send a Telegram message with jingle detection details
                    # send_telegram_message(bot_token, chat_id, f'[(^-^) (^-^)] Jingle Detected:\nName: {jingle_name}\nSimilarity: {similarity}\nIFrame Path: {iframe_path}\nIFrame URL: {iframe_url}\nDetection Time: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}')
                    send_telegram_message(
                        bot_token, 
                        chat_id,  
                        f"🎵 Jingle Detected 🎵 \n \
                        Name: {jingle_name}\n \
                        Similarity: {similarity}\n \
                        IFrame Path: {iframe_path}\n \
                        IFrame URL: {iframe_url}\n \
                        Detection Time: { start_datetime.strftime("%Y-%m-%d %H:%M:%S") }" 
                    )
                    # ! New New New New   
                    # Check if this is the start of an ad break
                    if self.ad_break_start_time is None: 
                        # Start of an ad break
                        self.ad_break_start_time = start_datetime
                    else:
                        # End of an ad break
                        ad_break_duration = (start_datetime - self.ad_break_start_time).total_seconds()
                        # Check if the ad break duration is greater than 3 seconds
                        if ad_break_duration > 3 and ad_break_duration <= 330 :
                            logger.info(f"Ad break detected. Start time: {self.ad_break_start_time}, End time: {start_datetime}, Duration: {ad_break_duration}")
                            #  Update Add More Req With New Attrubute channel-zone
                            data_nl = {
                                "start": str(self.ad_break_start_time),
                                "end": str(start_datetime),
                                "duration": str(ad_break_duration),
                                "channel": channel_name,
                                "region": "Netherlands"
                            }
                            data_fr = {
                                "start": str(self.ad_break_start_time),
                                "end": str(start_datetime),
                                "duration": str(ad_break_duration),
                                "channel": channel_name,
                                "region": "France"
                            }
                            # Send Live Info to DAI 
                            self.send_data(data_nl)
                            self.send_data(data_fr)
                            # initilize the ad break start time with jingle detection timz
                            self.ad_break_start_time = None
                        else:
                            logger.info(f"Ad break detected. Start time: {self.ad_break_start_time}, End time: {start_datetime}, Duration: {ad_break_duration}")
                            # 
                            data_nl = {
                                "start": str(self.ad_break_start_time),
                                "end": str(start_datetime),
                                "duration": str(ad_break_duration),
                                "channel": channel_name,
                                "region": "Netherlands"
                            }
                            data_fr = {
                                "start": str(self.ad_break_start_time),
                                "end": str(start_datetime),
                                "duration": str(ad_break_duration),
                                "channel": channel_name,
                                "region": "France"
                            }
                            # Send Live Info to DAI 
                            self.send_data(data_nl)
                            self.send_data(data_fr)
                            # initilize the ad break start time with jingle detection timz
                            self.ad_break_start_time = start_datetime

                # Check if self.ad_break_start_time is not None and a new TS file is created. If the difference between self.ad_break_start_time and datetime.now() 
                # is greater than 350 seconds, reset self.ad_break_start_time to None.

                if self.ad_break_start_time is not None:
                    time_difference = (datetime.now() - self.ad_break_start_time).total_seconds()
                    if time_difference >= 330:
                        # List of ad-duration options
                        # ad_duration_options = [10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95, 100, 105, 110, 115, 120, 125, 130, 135, 140, 145, 150, 155, 160, 165, 170, 175, 180, 185, 190, 195, 200, 205, 210, 215, 220, 225, 230, 235, 240, 245, 250, 255, 260, 265, 270, 275, 280, 285, 290, 295, 300]
                        ad_duration_options = [15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95, 100, 105, 110, 115, 120, 125, 130, 135, 140, 145, 150, 155, 160, 165, 170, 175]
                        # Generate random ad-duration
                        ad_duration_random = random.choice(ad_duration_options) 
                        # send message Telegram
                        ad_break_message = (
                             "🔊 Jingle Detected, End Not Detected 🔊\n"
                            f"🚨 Ad Break Detected With Random Duration 🚨\n"
                            f"🕒 With Static Duration: {ad_duration_random} Seconds\n" 
                            f"🕒 Detection Time: {self.ad_break_start_time.strftime('%Y-%m-%d %H:%M:%S')}"
                            f"🕒 Stop Time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
                            f"🕒 Taken Time: {time_difference}"
                        )
                        send_telegram_message(
                            bot_token, 
                            chat_id,  
                            ad_break_message
                        ) 
                        # 
                        data_nl = {
                            "start": str(self.ad_break_start_time),
                            "end": str(datetime.now()),
                            "duration": ad_duration_random, 
                            "channel": channel_name,
                            "region": "Netherlands"
                        }
                        data_fr = {
                            "start": str(self.ad_break_start_time),
                            "end": str(datetime.now()),
                            "duration": ad_duration_random, 
                            "channel": channel_name,
                            "region": "France"
                        }
                        # Send Live Info to DAI 
                        self.send_data(data_nl)
                        self.send_data(data_fr)
                        # Initialise The Ad Break Start Time To None
                        self.ad_break_start_time = None
                            
                # else:
                    # send_telegram_message(bot_token, chat_id, "No jingle detected.")
            except Exception as e:
                logger.error(f"Error processing file {event.src_path}: {e}")

            # Add the .ts file to the playlist
            self.add_ts_file_to_playlist(os.path.basename(event.src_path), self.get_media_duration(event.src_path)) 
            # Update the media sequence based on the first line in the .ts file
            self.update_media_sequence(self.extract_first_ts_line())
            # Delete the first .ts file if the segment count exceeds the limit  
            self.delete_ts_file_from_playlist() if self.count_segments() >= self.segment_limit + 1 else None
        
    def send_data(self,data):
        try:
            
            url = "http://207.180.254.4:9090/DAIManagement/dynamic_playlist/"  
            response = requests.get(url, params=data)
            if response.status_code == 200:
                print("Message sent successfully.")
                return True
            else:
                logger.info(f"Failed to send message. Status code: {response.status_code}")
                return False
        except Exception as e:
            logger.info(f"An error occurred: {e}")
            return False

    def get_media_duration(self, file_path):
        """
        Get the duration of the media file.

        Args:
            file_path (str): The path to the media file.

        Returns:
            float or None: The duration of the media file in seconds, or None if an error occurs.
        """
        try:
            clip = VideoFileClip(file_path)  # Load the video clip using MoviePy
            duration = clip.duration  # Get the duration of the clip
            clip.close()  # Close the clip to release resources
            return duration  # Return the duration of the clip
        except Exception as e:
            # Log an error message if an exception occurs during duration retrieval
            logger.error(f"Error while getting duration for {file_path}: {e}")
            return None  # Return None if an error occurs

    def count_segments(self):
        """
        Count the number of segments in the playlist file.

        Returns:
            int: The number of segments found in the playlist file.
        """
        segment_count = 0  # Initialize the segment count
        # Open the playlist file for reading ('r')
        with open(self.playlist_file, 'r') as f:
            previous_line = None  # Initialize the variable to store the previous line
            # Iterate through each line in the file
            for line in f:
                # Check if the current line contains a .ts filename and the previous line contains #EXTINF
                if line.strip().endswith(".ts") and previous_line and previous_line.startswith('#EXTINF'):
                    # Increment the segment count if the conditions are met
                    segment_count += 1
                # Update the previous line to the current line
                previous_line = line.strip()
        return segment_count  # Return the total segment count 
    
    def extract_first_ts_line(self):
        """
        Extract the first line containing a .ts filename after a line with #EXTINF from the playlist file.

        Returns:
            int: The value extracted from the first line.
        """
        # Open the playlist file for reading ('r')
        with open(self.playlist_file, 'r') as f:
            previous_line = None  # Initialize the variable to store the previous line
            # Iterate through each line in the file
            for line in f:
                # Check if the current line contains a .ts filename and the previous line contains #EXTINF
                if re.search(r'^[^#].*\.ts$', line.strip()) and previous_line and previous_line.startswith('#EXTINF'):
                    # Extract the value from the current line using regular expression and return it
                    return re.findall(r'\d+', line.strip())[0]
                # Update the previous line to the current line
                previous_line = line.strip()
        # Return 0 if no matching line is found
        return 0
    
    def update_media_sequence(self, hls_media_sequence):
        """
        Update the #EXT-X-MEDIA-SEQUENCE tag in the playlist file based on the added files.

        Args:
            hls_media_sequence (int): The new value for the #EXT-X-MEDIA-SEQUENCE tag.
        """
        # Open the playlist file for reading and writing ('r+')
        with open(self.playlist_file, 'r+') as f:
            # Read all lines from the playlist file
            lines = f.readlines()
            # Iterate through each line in the file
            for i, line in enumerate(lines):
                # Check if the line starts with "#EXT-X-MEDIA-SEQUENCE:"
                if line.startswith("#EXT-X-MEDIA-SEQUENCE:"):
                    # Update the line with the new value for #EXT-X-MEDIA-SEQUENCE
                    lines[i] = f"#EXT-X-MEDIA-SEQUENCE:{hls_media_sequence}\n"
                    # Break out of the loop since the update is done
                    break
            # Move the file pointer to the beginning of the file
            f.seek(0)
            # Write the updated lines back to the file
            f.writelines(lines)
            # Truncate the file to remove any remaining content beyond the new data written
            f.truncate()
        # Print a log message indicating the update (commented out)
        # logger.info(f"Updated #EXT-X-MEDIA-SEQUENCE to {self.hls_media_sequence}")

    def add_ts_file_to_playlist(self, filename, duration):
        """
        Adds a .ts file entry to the playlist file with the specified filename and duration.

        Args:
            filename (str): The filename of the .ts file to be added to the playlist.
            duration (float): The duration of the .ts file in seconds.
        """
        # Open the playlist file in 'a' (append) mode
        with open(self.playlist_file, 'a') as f:
            # Write the duration of the .ts file in the EXTINF tag format
            f.write(f"#EXTINF:{duration}0000,\n")
            # Write the filename of the .ts file to the playlist
            f.write(f"{filename}\n")
        # Print a message indicating the addition of the file to the playlist (commented out)
        # print(f"Added {filename} to playlist")
 
    def delete_ts_file_from_playlist(self):
        """
        Deletes the first .ts file entry from the playlist file.
        """
        # Open the playlist file for reading and writing ('r+')
        with open(self.playlist_file, 'r+') as f:
            # Read the content of the playlist file
            content = f.read()
            # Move the file pointer to the beginning of the file
            f.seek(0)
            
            # Check if any .ts file exists between #EXT-X-CUE-OUT and #EXT-X-CUE-IN
            if re.search(r'#EXT-X-CUE-OUT.*?\n#EXTINF:[^\n]+\n([^#]+\n).*?#EXT-X-CUE-IN', content, flags=re.DOTALL):
                # Use regular expression to find and replace the first occurrence of lines 
                # containing '#EXT-X-CUE-OUT', '#EXTINF' and the following line starting with a filename ending with '.ts', 
                # and lines containing '#EXT-X-CUE-IN', effectively removing the entry from the playlist
                f.write(re.sub(r'#EXT-X-CUE-OUT.*?\n#EXTINF:[^\n]+\n([^#]+\n).*?#EXT-X-CUE-IN', '', content, flags=re.DOTALL))
            else:
                # Use regular expression to find and replace the first occurrence of a line containing '#EXTINF' and the following line starting with a filename ending with '.ts'
                # Replace it with an empty string, effectively removing the entry from the playlist
                f.write(re.sub(r'#EXTINF:[^\n]+\n([^#]+\n)', '', content, count=1, flags=re.DOTALL))
            
            # Truncate the file to remove any remaining content beyond the new data written
            f.truncate() 
            
        # Update the media sequence based on the first line in the .ts file
        self.update_media_sequence(int(self.extract_first_ts_line()))
 

def main():
    """
    Main function to start monitoring the specified folder for .ts files 
    and detect ad breaks using jingle recognition.
    """

    # Call the function to create the output directory
    create_output_directory(output_path, channel_name)
    # Wait until index.m3u8 is created before proceeding
    wait_for_index_m3u8(output_path, channel_name)
    # Ensure the playlist file exists; create an initial playlist file if it does not exist
    ensure_playlist_m3u8_exists(playlist_file_path)
    # Ensure the master file exists for HLS streaming
    ensure_master_m3u8_exists(folder_to_watch)
    
    # Specify whether jingles are being used for ad break detection
    use_jingles = True 
    # Initialize the event handler for .ts files
    event_handler = TsFilesHandler(playlist_file_path, max_ts_file_number, use_jingles) 
    # Initialize the observer to monitor the folder for file system events
    observer = Observer()
    # Schedule the event handler to be triggered when changes occur in the specified folder
    observer.schedule(event_handler, folder_to_watch, recursive=True)
    # Start monitoring the folder
    observer.start()
    
    try:
        # Keep the script running indefinitely
        while True:
            # Sleep to avoid consuming too much CPU
            time.sleep(1)
    except KeyboardInterrupt:
        # Stop the observer if the user interrupts the script
        observer.stop()
    
    # Wait for the observer to terminate
    observer.join()

    
if __name__ == "__main__":
    # Print a message to the console 
    logger.info(f"Starting Monitoring On Folder <{folder_to_watch}> ...")
    # Run the main function: Call the main function to execute the script. 
    main()