import calendar
import os
from fastapi import HTTPException
from fastapi.responses import JSONResponse
import pymongo
import jwt
import logging
import datetime
import random
from uuid import uuid4
from exponent_server_sdk import DeviceNotRegisteredError, PushClient, PushMessage, PushServerError, PushTicketError

from dotenv import load_dotenv

load_dotenv()

MONGODB_DATABASE = os.getenv("MONGODB_DATABASE")


def mongodb_connect():
    mongodb_host = os.getenv("MONGODB_HOST")
    mongodb_port = os.getenv("MONGODB_PORT")
    mongodb_database = os.getenv("MONGODB_DATABASE")
    mongodb_user = os.getenv("MONGODB_USER")
    mongodb_password = os.getenv("MONGODB_PASSWORD")
    mongodb_client = pymongo.MongoClient(
        # tls=True,
        # tlsInsecure=True,
        host=mongodb_host,
        port=int(mongodb_port),
        authSource=mongodb_database,
        username=mongodb_user,
        password=mongodb_password,
    )
    return mongodb_client


mongodb_client = mongodb_connect()


def decode_auth_token(auth_token):
    payload = jwt.decode(auth_token, os.getenv("JWT_SECRET"), algorithms=["HS256"])
    return payload


def user_exist(auth_token):
    sub = get_user_sub(auth_token)
    result = mongodb_client[MONGODB_DATABASE]["users"].find_one(
        {"sub": sub}, {"_id": 0, "sub": 1}
    )
    if not result:
        return JSONResponse(content={"status": "error", "message": "Erreur. Utilisateur n'existe pas"}, status_code=404)

    return True


def check_access(auth_token, admited_roles):
    if not auth_token:
        logging.warning("not authorized")
        raise HTTPException(status_code=401, detail="Not Authorized. You must provide a token. You must log in to generate a token.")
    if not user_exist(auth_token):
        return False
    if is_root(auth_token):
        return True

    if "*" in admited_roles:
        return True
    user_sub = get_user_sub(auth_token)
    user = mongodb_client[MONGODB_DATABASE]["users"].find_one({"sub": user_sub})
    for role in user["roles"]:
        if role in admited_roles:
            return True
    raise HTTPException(status_code=403, detail="Forbidden. You don't have access to this resource")


def get_user_sub(auth_token):
    if not auth_token:
        logging.warning("Accès non autorisé")
        return JSONResponse(content={"status": "failed", "message": "Accès non autorisé. Vous devez fournir un jeton. Vous devez vous connecter pour générer un token"}, status_code=401)

    try:
        payload = decode_auth_token(auth_token)
        user_sub = payload["sub"]
        return user_sub
    except jwt.exceptions.DecodeError as err:
        return JSONResponse(content={"status": "failed", "message": str(err)}, status_code=401)
    except jwt.ExpiredSignatureError:
        return JSONResponse(content={"status": "failed", "message": "Accès non autorisé. Erreur de signature expirée."}, status_code=401)
    except jwt.InvalidTokenError:
        return JSONResponse(content={"status": "failed", "message": "Accès non autorisé. Erreur de jeton invalide."}, status_code=401)


def is_root(auth_token):
    if not auth_token:
        logging.warning("Accès non autorisé")
        return JSONResponse(content={"status": "failed", "message": "Accès non autorisé. Vous devez fournir un jeton. Vous devez vous connecter pour générer un token"}, status_code=401)
    try:
        payload = decode_auth_token(auth_token)
        user_sub = payload["sub"]
        user = mongodb_client[MONGODB_DATABASE]["users"].find_one(
            {"sub": user_sub})
        if not user:
            return False
        roles = user["roles"]
        return "root" in roles
    except jwt.exceptions.DecodeError as err:
        return JSONResponse(content={"status": "failed", "message": str(err)}, status_code=401)
    except jwt.ExpiredSignatureError:
        return JSONResponse(content={"status": "failed", "message": "Accès non autorisé. Erreur de signature expirée."}, status_code=401)
    except jwt.InvalidTokenError:
        return JSONResponse(content={"status": "failed", "message": "Accès non autorisé. Erreur de jeton invalide."}, status_code=401)


def get_perms(auth_token):
    if not auth_token:
        logging.warning("Accès non autorisé")
        return JSONResponse(content={"status": "failed", "message": "Accès non autorisé. Vous devez fournir un jeton. Vous devez vous connecter pour générer un token"}, status_code=401)
    try:
        payload = decode_auth_token(auth_token)
        user_sub = payload["sub"]
        user = mongodb_client[MONGODB_DATABASE]["users"].find_one(
            {"sub": user_sub})
        roles = user["roles"]
        perms = {}
        for role_name in roles:
            role = mongodb_client[MONGODB_DATABASE]["roles"].find_one(
                {"name": role_name}
            )
            for perm in role["permissions"]:
                perms[perm] = 1
        return perms
    except jwt.exceptions.DecodeError as err:
        return JSONResponse(content={"status": "failed", "message": str(err)}, status_code=401)
    except jwt.ExpiredSignatureError:
        return JSONResponse(content={"status": "failed", "message": "Accès non autorisé. Erreur de signature expirée."}, status_code=401)
    except jwt.InvalidTokenError:
        return JSONResponse(content={"status": "failed", "message": "Accès non autorisé. Erreur de jeton invalide."}, status_code=401)


def encode_auth_token(user_id, expired_in_seconds):
    payload = {
        "exp": datetime.datetime.utcnow()
        + datetime.timedelta(days=0, seconds=expired_in_seconds),
        "iat": datetime.datetime.utcnow(),
        "sub": user_id,
    }
    return jwt.encode(
        payload,
        os.getenv("JWT_SECRET"),
        algorithm="HS256",
    )


def get_roles(auth_token):
    if not auth_token:
        logging.warning("Accès non autorisé")
        return JSONResponse(content={"status": "failed", "message": "Accès non autorisé. Vous devez fournir un jeton. Vous devez vous connecter pour générer un token"}, status_code=401)
    try:
        payload = decode_auth_token(auth_token)
        user_sub = payload["sub"]
        user = mongodb_client[MONGODB_DATABASE]["users"].find_one(
            {"sub": user_sub})
        roles = user["roles"]
        return roles
    except jwt.exceptions.DecodeError as err:
        return JSONResponse(content={"status": "failed", "message": str(err)}, status_code=401)
    except jwt.ExpiredSignatureError:
        return JSONResponse(content={"status": "failed", "message": "Accès non autorisé. Erreur de signature expirée."}, status_code=401)
    except jwt.InvalidTokenError:
        return JSONResponse(content={"status": "failed", "message": "Accès non autorisé. Erreur de jeton invalide."}, status_code=401)

def get_sector(auth_token):
    user_sub = get_user_sub(auth_token)
    user = mongodb_client[MONGODB_DATABASE]["users"].find_one({"sub": user_sub})
    return user["sector"]

def is_admin(auth_token):
    roles = get_roles(auth_token)
    return "admin" in roles

days_map = {
    'Lun': 0, 'Mar': 1, 'Mer': 2, 'Jeu': 3, 'Ven': 4, 'Sam': 5, 'Dim': 6
}

def generate_interventions_for_client(client, pool, existing_interventions):
    interventions = []
    maintenance_info = client['maintenance_info']
    preferred_days = maintenance_info['days']
    preferred_hour = maintenance_info['hour']
    duration = int(maintenance_info['duration'])
    frequency = maintenance_info['frequency']

    today = datetime.date.today()
    end_date = today + datetime.timedelta(days=365*2) # Two years from today
    
    for month_info in frequency:
        month = int(month_info['month'])
        recurrence = int(month_info['reccurence'])
        period = month_info['period']
        
        current_year = today.year
        next_year = current_year + 1
        
        for year in [current_year, next_year]:
            month_start = datetime.date(year, month + 1, 1)
            if month_start < today:
                continue
            if month_start > end_date:
                break

            month_end = month_start.replace(day=calendar.monthrange(year, month + 1)[1])
            month_end = min(month_end, end_date)
            
            if period == 'month':
                # Check existing interventions for this month
                existing_month_interventions = [
                    e for e in existing_interventions 
                    if month_start <= datetime.datetime.strptime(e['date'], '%Y-%m-%d').date() <= month_end
                ]
                
                # Calculate how many more interventions we need
                interventions_needed = max(0, recurrence - len(existing_month_interventions))
                
                month_interventions = []
                for _ in range(interventions_needed):
                    available_days = [day for day in range(7) if day not in [
                        datetime.datetime.strptime(i['date'], '%Y-%m-%d').weekday() 
                        for i in existing_month_interventions + month_interventions
                    ]]
                    intervention_date = generate_intervention_date(month_start, month_end, preferred_days, preferred_hour, available_days)
                    
                    # Ensure we're not creating an intervention on the same day
                    while any(i['date'] == intervention_date.strftime('%Y-%m-%d') for i in month_interventions):
                        available_days = [d for d in available_days if d != intervention_date.weekday()]
                        if not available_days:
                            available_days = list(range(7))  # If all days are taken, allow repeats but on a different week
                        intervention_date = generate_intervention_date(month_start, month_end, preferred_days, preferred_hour, available_days)
                    
                    month_interventions.append(create_intervention(intervention_date, duration, pool['sub'], client['user_sub']))
                
                interventions.extend(month_interventions)

            elif period == 'week':
                weeks = calendar.monthcalendar(year, month + 1)
                for week in weeks:
                    week_start = datetime.date(year, month + 1, week[0] or 1)
                    week_end = datetime.date(year, month + 1, week[6] or month_end.day)
                    
                    # Check existing interventions for this week
                    existing_week_interventions = [
                        e for e in existing_interventions 
                        if week_start <= datetime.datetime.strptime(e['date'], '%Y-%m-%d').date() <= week_end
                    ]
                    
                    # Calculate how many more interventions we need
                    interventions_needed = max(0, recurrence - len(existing_week_interventions))
                    
                    week_interventions = []
                    for _ in range(interventions_needed):
                        available_days = [day for day in range(7) if day not in [
                            datetime.datetime.strptime(i['date'], '%Y-%m-%d').weekday() 
                            for i in existing_week_interventions + week_interventions
                        ]]
                        if not available_days:
                            available_days = list(range(7))  # If all days are taken, allow repeats
                        intervention_date = generate_intervention_date(week_start, week_end, preferred_days, preferred_hour, available_days)
                        week_interventions.append(create_intervention(intervention_date, duration, pool['sub'], client['user_sub']))
                    
                    interventions.extend(week_interventions)

            elif period == 'day':
                current_date = month_start
                while current_date <= month_end:
                    for _ in range(recurrence):
                        intervention_date = generate_intervention_date(current_date, current_date, preferred_days, preferred_hour)
                        interventions.append(create_intervention(intervention_date, duration, pool['sub'], client['user_sub']))
                    current_date += datetime.timedelta(days=1)

    # Remove duplicates and sort interventions
    interventions.sort(key=lambda x: (x['date'], x['hour']))
    unique_interventions = []
    for intervention in interventions:
        if not any(i['date'] == intervention['date'] and i['hour'] == intervention['hour'] for i in unique_interventions):
            unique_interventions.append(intervention)

    # Remove existing interventions
    existing_intervention_dates = set((e['date'], e['hour']) for e in existing_interventions)
    unique_interventions = [i for i in unique_interventions if (i['date'], i['hour']) not in existing_intervention_dates]

    return unique_interventions

def generate_intervention_date(start_date, end_date, preferred_days, preferred_hour, available_days=None):
    days_map = {'Lun': 0, 'Mar': 1, 'Mer': 2, 'Jeu': 3, 'Ven': 4, 'Sam': 5, 'Dim': 6}
    preferred_weekdays = [days_map[day] for day in preferred_days]
    
    available_dates = [start_date + datetime.timedelta(days=i) for i in range((end_date - start_date).days + 1)]
    if available_days is not None:
        available_dates = [date for date in available_dates if date.weekday() in available_days]
    
    # First, try to find a date from preferred days
    for weekday in preferred_weekdays:
        possible_dates = [date for date in available_dates if date.weekday() == weekday]
        if possible_dates:
            chosen_date = possible_dates[0]
            return datetime.datetime.combine(chosen_date, datetime.time.fromisoformat(preferred_hour))
    
    # If no preferred day is available, go through all days of the week in order
    for weekday in range(7):
        if available_days is not None and weekday in available_days:
            possible_dates = [date for date in available_dates if date.weekday() == weekday]
            if possible_dates:
                chosen_date = possible_dates[0]
                return datetime.datetime.combine(chosen_date, datetime.time.fromisoformat(preferred_hour))
    
    # If we still haven't found a date, just take the first available date
    if available_dates:
        chosen_date = available_dates[0]
    else:
        chosen_date = start_date  # Fallback to start_date if no available dates
    
    return datetime.datetime.combine(chosen_date, datetime.time.fromisoformat(preferred_hour))

def create_intervention(intervention_date, duration, pool_sub, user_sub):
    return {
        'sub': str(uuid4()),
        'date': intervention_date.strftime('%Y-%m-%d'),
        'hour': intervention_date.strftime('%H:%M'),
        'duration': str(duration),
        'pool_sub': pool_sub,
        'user_sub': user_sub
    }

def send_push_notification(user_subs, title, message):
    users = [user["sub"] for user in user_subs]
    for user_sub in users:
        tokens = list(mongodb_client[MONGODB_DATABASE]["push_tokens"].find(
            {"user_sub": user_sub},
            {"_id": 0, "token": 1}
        ))
        for token_doc in tokens:
            token = token_doc["token"]
            try:
                response = PushClient().publish(
                    PushMessage(to=token,
                                title=title,
                                body=message,
                                data={"type": "anomaly_created"})
                )
            except Exception as e:
                print(f"Error: {str(e)}")
        
        notification_data = {
            "sub": str(uuid4()),
            "user_sub": user_sub,
            "title": title,
            "message": message,
            "sent_at": str(datetime.datetime.now()),
            "status": "sent"
        }
        mongodb_client[MONGODB_DATABASE]["notifications"].insert_one(notification_data)

def process_interventions_by_period(existing_interventions, month_info, current_year):
    month = int(month_info['month'])
    recurrence = int(month_info['reccurence'])
    period = month_info['period']

    # Define the start and end dates for the current month
    month_start = datetime.date(current_year, month + 1, 1)
    month_end = month_start.replace(day=calendar.monthrange(current_year, month + 1)[1])

    # Filter existing interventions for this period
    period_interventions = [
        i for i in existing_interventions 
        if month_start <= datetime.datetime.strptime(i['date'], '%Y-%m-%d').date() <= month_end
    ]

    if period == 'month':
        # Keep only the first 'recurrence' number of interventions for the month
        interventions_to_keep = sorted(period_interventions, key=lambda x: x['date'])[:recurrence]
    elif period == 'week':
        # Group interventions by week and keep only 'recurrence' number for each week
        weeks = calendar.monthcalendar(current_year, month + 1)
        interventions_to_keep = []
        for week in weeks:
            week_start = datetime.date(current_year, month + 1, week[0] or 1)
            week_end = datetime.date(current_year, month + 1, week[6] or month_end.day)
            week_interventions = [
                i for i in period_interventions 
                if week_start <= datetime.datetime.strptime(i['date'], '%Y-%m-%d').date() <= week_end
            ]
            interventions_to_keep.extend(sorted(week_interventions, key=lambda x: x['date'])[:recurrence])
    elif period == 'day':
        # Keep only 'recurrence' number of interventions for each day
        interventions_to_keep = []
        current_date = month_start
        while current_date <= month_end:
            day_interventions = [
                i for i in period_interventions 
                if datetime.datetime.strptime(i['date'], '%Y-%m-%d').date() == current_date
            ]
            interventions_to_keep.extend(sorted(day_interventions, key=lambda x: x['hour'])[:recurrence])
            current_date += datetime.timedelta(days=1)

    # Determine interventions to remove
    interventions_to_remove = [
        i for i in period_interventions 
        if i not in interventions_to_keep and 'state' not in i
    ]

    return interventions_to_remove