from datetime import datetime

import pytz
from dateutil.relativedelta import relativedelta
from django.core.cache import cache
from django.db.models import Sum
from django.utils import timezone
from django.utils.timezone import make_aware

from ledgers.ledgers_helper import generate_chart_of_account_code
from ledgers.models import OrganisationSubAccount, OrganisationBranch
from ledgers.serializers import *
from license.models import *
from notifications.notifications_helper import *
from organisations.models import OrganisationSetting, Organisation
from questbanker_api.utils import send_email
from savings.models import *


LICENSE_EXPIRED_ACCESS_MESSAGE = "You can't access the system because your organisation license has expired. Please renew the license to continue."
INACTIVE_ORGANISATION_ACCESS_MESSAGE = "Your organisation is inactive. Please contact support."
ORG_LICENSE_EXPIRY_CACHE_KEY = "organisation-license-expiry:{organisation_id}"
LICENSE_EXEMPT_ORGANISATION_IDS = {1}
LICENSE_EXEMPT_SYNC_REASON = "license_exempt_organisation"


def _normalise_datetime(value):
    if not value:
        return None

    if timezone.is_naive(value):
        return make_aware(value, timezone.get_current_timezone())

    return value


def _normalise_organisation_id(value):
    if hasattr(value, 'id'):
        value = getattr(value, 'id', None)

    if value in [None, '']:
        return None

    try:
        return int(value)
    except (TypeError, ValueError):
        return value


def _get_organisation_cache_key(organisation_id):
    return ORG_LICENSE_EXPIRY_CACHE_KEY.format(organisation_id=organisation_id)


def is_license_exempt_organisation(organisation=None, organisation_id=None):
    resolved_organisation_id = _normalise_organisation_id(organisation)
    if resolved_organisation_id is None:
        resolved_organisation_id = _normalise_organisation_id(organisation_id)

    return resolved_organisation_id in LICENSE_EXEMPT_ORGANISATION_IDS


def _ensure_license_exempt_organisation_is_active(organisation=None, organisation_id=None):
    resolved_organisation_id = _normalise_organisation_id(organisation)
    if resolved_organisation_id is None:
        resolved_organisation_id = _normalise_organisation_id(organisation_id)

    if resolved_organisation_id is None:
        return None

    if organisation is None or getattr(organisation, 'id', None) != resolved_organisation_id:
        organisation = Organisation.objects.filter(pk=resolved_organisation_id).first()

    if organisation and str(organisation.status or '').lower() != 'active':
        Organisation.objects.filter(pk=organisation.id).update(status='active')
        organisation.status = 'active'

    if organisation:
        cache.delete(_get_organisation_cache_key(organisation.id))

    return {
        "organisation_id": resolved_organisation_id,
        "organisation_status": "active",
        "has_license": False,
        "has_valid_license": True,
        "latest_end_date": None,
        "reason": LICENSE_EXEMPT_SYNC_REASON,
        "is_license_exempt": True,
    }


def sync_organisation_license_status(organisation_id=None):
    resolved_organisation_id = _normalise_organisation_id(organisation_id)

    if is_license_exempt_organisation(organisation_id=resolved_organisation_id):
        return _ensure_license_exempt_organisation_is_active(organisation_id=resolved_organisation_id)

    current_time = timezone.now()
    license_queryset = LicenseSubscription.objects.select_related('organisation').order_by('organisation_id', '-end_date', '-id')

    if resolved_organisation_id not in [None, '']:
        license_queryset = license_queryset.filter(organisation_id=resolved_organisation_id)

    if LICENSE_EXEMPT_ORGANISATION_IDS:
        license_queryset = license_queryset.exclude(organisation_id__in=LICENSE_EXEMPT_ORGANISATION_IDS)

    grouped_licenses = {}
    for subscription in license_queryset:
        grouped_licenses.setdefault(subscription.organisation_id, []).append(subscription)

    sync_results = []
    if resolved_organisation_id in [None, '']:
        for exempt_organisation_id in LICENSE_EXEMPT_ORGANISATION_IDS:
            exempt_result = _ensure_license_exempt_organisation_is_active(organisation_id=exempt_organisation_id)
            if exempt_result:
                sync_results.append(exempt_result)

    if not grouped_licenses:
        if resolved_organisation_id not in [None, '']:
            organisation = Organisation.objects.filter(pk=resolved_organisation_id).first()
            if organisation:
                cache.delete(_get_organisation_cache_key(organisation.id))
                return {
                    "organisation_id": organisation.id,
                    "organisation_status": organisation.status,
                    "has_license": False,
                    "has_valid_license": False,
                    "latest_end_date": None,
                    "reason": "no_license",
                }

            return {
                "organisation_id": resolved_organisation_id,
                "organisation_status": None,
                "has_license": False,
                "has_valid_license": False,
                "latest_end_date": None,
                "reason": "organisation_missing",
            }

        return sync_results

    for current_organisation_id, organisation_licenses in grouped_licenses.items():
        organisation = organisation_licenses[0].organisation
        latest_end_date = None
        has_valid_license = False

        for current_license in organisation_licenses:
            end_date = _normalise_datetime(current_license.end_date)
            if end_date and (latest_end_date is None or end_date > latest_end_date):
                latest_end_date = end_date

            desired_status = 'inactive'
            if current_license.force_inactive:
                desired_status = 'inactive'
            elif end_date and end_date >= current_time:
                desired_status = 'active'
                has_valid_license = True

            if current_license.status != desired_status:
                LicenseSubscription.objects.filter(pk=current_license.id).update(status=desired_status)

        organisation_status = 'active' if has_valid_license else 'inactive'
        if str(organisation.status or '').lower() != organisation_status:
            Organisation.objects.filter(pk=organisation.id).update(status=organisation_status)
            organisation.status = organisation_status

        cache_key = _get_organisation_cache_key(organisation.id)
        if latest_end_date:
            cache.set(cache_key, latest_end_date.isoformat(), timeout=None)
        else:
            cache.delete(cache_key)

        sync_results.append({
            "organisation_id": organisation.id,
            "organisation_status": organisation_status,
            "has_license": True,
            "has_valid_license": has_valid_license,
            "latest_end_date": latest_end_date,
            "reason": "valid_license" if has_valid_license else "expired_license",
        })

    if resolved_organisation_id not in [None, '']:
        return sync_results[0]

    return sync_results


def should_sync_organisation_license(organisation):
    if not organisation or not getattr(organisation, 'id', None):
        return False

    if is_license_exempt_organisation(organisation=organisation):
        return False

    cached_expiry = cache.get(_get_organisation_cache_key(organisation.id))
    if not cached_expiry:
        return True

    try:
        if isinstance(cached_expiry, str):
            cached_expiry = datetime.fromisoformat(cached_expiry)
        cached_expiry = _normalise_datetime(cached_expiry)
    except Exception:
        return True

    if str(organisation.status or '').lower() != 'active':
        return True

    return cached_expiry <= timezone.now()


def get_organisation_access_state(organisation=None, organisation_id=None, force_sync=False):
    if organisation is None and organisation_id not in [None, '']:
        organisation = Organisation.objects.filter(pk=organisation_id).first()

    resolved_organisation_id = getattr(organisation, 'id', None) or organisation_id
    access_state = {
        "allowed": True,
        "message": "",
        "reason": "allowed",
        "organisation_id": resolved_organisation_id,
    }

    if organisation is None:
        if resolved_organisation_id in [None, '']:
            return access_state

        access_state.update({
            "allowed": False,
            "message": INACTIVE_ORGANISATION_ACCESS_MESSAGE,
            "reason": "organisation_missing",
        })
        return access_state

    if is_license_exempt_organisation(organisation=organisation, organisation_id=resolved_organisation_id):
        sync_result = _ensure_license_exempt_organisation_is_active(
            organisation=organisation,
            organisation_id=resolved_organisation_id,
        )
        if sync_result:
            access_state["sync_result"] = sync_result
        return access_state

    sync_result = None
    if force_sync or should_sync_organisation_license(organisation):
        sync_result = sync_organisation_license_status(organisation.id)
        access_state["sync_result"] = sync_result
        if sync_result and sync_result.get("organisation_status"):
            organisation.status = sync_result["organisation_status"]

    if str(organisation.status or '').lower() == 'active':
        return access_state

    force_inactive = LicenseSubscription.objects.filter(
        organisation_id=resolved_organisation_id, force_inactive=True
    ).exists()

    if force_inactive:
        access_state.update({
            "allowed": False,
            "message": INACTIVE_ORGANISATION_ACCESS_MESSAGE,
            "reason": "organisation_inactive",
        })
        return access_state

    expired_license = False
    if sync_result:
        expired_license = bool(sync_result.get("has_license")) and not bool(sync_result.get("has_valid_license"))
    else:
        cached_expiry = cache.get(_get_organisation_cache_key(organisation.id))
        if cached_expiry:
            try:
                if isinstance(cached_expiry, str):
                    cached_expiry = datetime.fromisoformat(cached_expiry)
                cached_expiry = _normalise_datetime(cached_expiry)
                expired_license = bool(cached_expiry and cached_expiry <= timezone.now())
            except Exception:
                expired_license = False

    access_state.update({
        "allowed": False,
        "message": LICENSE_EXPIRED_ACCESS_MESSAGE if expired_license else INACTIVE_ORGANISATION_ACCESS_MESSAGE,
        "reason": "license_expired" if expired_license else "organisation_inactive",
    })
    return access_state


def get_license_charts(organisation_id):
    setting_key = "{organisation_id}_organisation_license_chart".format(organisation_id=organisation_id)
    org_wallet_setting = OrganisationSetting.objects.filter(org_setting_id=organisation_id, setting_key=setting_key).first()
    
    if org_wallet_setting:
        return OrganisationSubAccount.objects.filter(id=int(org_wallet_setting.setting_value)).first()
    
    # Create chart if not exists
    organisation = Organisation.objects.filter(id=organisation_id).first()
    account_name = "License - {organisation_name}".format(organisation_name=organisation.name)
    parent_chart = OrganisationSubAccount.objects.filter(account_code='sys-425', account_organisation_id=organisation_id).first()
    if parent_chart:
        account_code = generate_chart_of_account_code(parent_chart.id, 'income', organisation_id)
        organisation_license_ledger = OrganisationSubAccount(account_name=account_name, account_line='income', account_code=account_code, account_organisation_id=organisation_id, account_type='locked_chart', parent_id=parent_chart, allow_sub_accounts=False)
        organisation_license_ledger.save()
        OrganisationSetting.objects.create(org_setting=organisation, setting_key=setting_key, setting_value=organisation_license_ledger.id, setting_added_by=1)

    return organisation_license_ledger


def license_payments_reminder():
    try:
        org_license_settings   = OrganisationSetting.objects.filter(setting_key='license_reminder').all()
        cron_organisations = []
        for org_license_setting in org_license_settings:
            organisation_reminder_settings = eval(org_license_setting.setting_value)

            if org_license_setting.setting_value and len(organisation_reminder_settings) > 0:
                days_before_due_date = int(organisation_reminder_settings['days_before_due_date']) if 'days_before_due_date' in organisation_reminder_settings and organisation_reminder_settings['days_before_due_date'] !='' else 0
                days_after_due_date = int(organisation_reminder_settings['days_after_due_date']) if 'days_after_due_date' in organisation_reminder_settings and organisation_reminder_settings['days_after_due_date'] !='' else 0
                include_due_date = organisation_reminder_settings['include_due_date'] if 'include_due_date' in organisation_reminder_settings and organisation_reminder_settings['include_due_date'] !='' else 'no'
                organisation = org_license_setting.org_setting
                if days_before_due_date == 0 and days_after_due_date == 0 and include_due_date == 'no':
                    continue

                # get organisation loans
                cron_organisations.append(organisation.name)
                active_members = SavingAccount.objects.filter(customer_branch__branch_organisation=organisation,status='active').count()
                licenses = LicenseSubscription.objects.filter(organisation=organisation,status='active')
                for license_details in licenses:
                    before_due_date = None
                    after_due_date = None
                    after_due_arrear_days = None
                    amount  = license_details.amount
                    license_id = license_details.id
                    method_type = license_details.calculation_types
                    current = timezone.now()
                    eat_timezone = pytz.timezone("Africa/Nairobi")
                    end_date = datetime.strptime(license_details.end_date.astimezone(eat_timezone).strftime('%Y-%m-%d %H:%M:%S'), '%Y-%m-%d %H:%M:%S')
                    current_date =  datetime.strptime(current.astimezone(eat_timezone).strftime('%Y-%m-%d %H:%M:%S'), '%Y-%m-%d %H:%M:%S')
                    payment_date = datetime.strptime(license_details.end_date.astimezone(eat_timezone).strftime('%Y-%m-%d %H:%M:%S'), '%Y-%m-%d %H:%M:%S')

                    delta = relativedelta(days=days_before_due_date)
                    current_date_before_due = current_date + delta
                    after_due_date = payment_date
                    
                    daysToGo = (end_date - current_date).days
                    payments = LicensePayments.objects.filter(license_subscription=license_id).aggregate(amount_paid=Sum('amount_paid'))
                    paid_amount = payments.get('amount_paid')
                    if method_type == 'annual-flat':
                        amount_due = amount - paid_amount
                    amount_due = (amount*active_members)-paid_amount
                    if current_date_before_due == payment_date and days_before_due_date > 0:
                        license_date = license_details.end_date.astimezone(eat_timezone)
                        before_due_date = license_date.strftime('%Y-%m-%d')

                    if current_date == payment_date and include_due_date == 'yes':
                        before_due_date = 'today'
                        
                    if days_after_due_date > 0:
                        arrear_days = (current_date - payment_date).days
                        if arrear_days > 0 and arrear_days % days_after_due_date == 0:
                            after_due_date = license_details.end_date.strftime('%Y-%m-%d') if after_due_date == None else after_due_date
                            after_due_arrear_days = arrear_days if after_due_arrear_days == None else after_due_arrear_days

                    if after_due_date and after_due_arrear_days and amount_due > 0:
                        data  = f"Dear, {license_details.organisation.name}, your license of amount" + amount_due + " is due in:"+ daysToGo + " Days .as at " + str(after_due_arrear_days) + "arrear days"
                        send_email(data, 'License Payment Reminder.', license_details.organisation.email)
                    elif before_due_date == 'today':
                        data  = f"Dear, {license_details.organisation.name}, your license of amount {amount_due} is due today " 
                        send_email(data, 'License Payment Reminder.', license_details.organisation.email)
                    elif before_due_date:
                        data  = f"Dear, {license_details.organisation.name}, your license of amount" + amount_due +" will be due on" + str(before_due_date)
                        send_email(data, 'License Payment Reminder.', license_details.organisation.email)

                    save_user_notification({
                        "heading":  "License Payment Reminder",
                        "message": f"License Payment Reminder has been sent to: {license_details.organisation.name} as at {license_details.date_added.date()}",
                        "added_by":None,
                        "last_updated_by":None,
                        "key":"license_notifications"
                    })
    
                    organCombinedString = ','.join(cron_organisations)
                    send_email('License payment reminders cron completed successfully for [ ' + organCombinedString + ' ]', 'License Payment Reminders Cron.', )
                    
    except Exception as e:
        send_email('License payment reminders cron failed ' + str(e), 'License Payment Reminders Cron.' )
