import base64
import json
import logging
import urllib
from datetime import datetime, timedelta
import jwt
import bugsnag
import traceback
from flask import request
from urllib.parse import quote_plus
import pytz
import config
from emi_repository import EMIRepository
from utils.Utilities import Utilities
from utils.validation import *
from utils.hashing import hashids
from utils.cache import lru_cache
from utils.s3_upload import s3_upload, s3_read_file, upload_presigned_url, s3_upload_update
from exceptions.custom_exceptions import CustomExceptions
from accessor.bureau_service import call_bureau_service
from utils.shortio import create_short_url
from accessor.digio import create_template, regenerate_token
import accessor.creditfair as creditfair
from utils.php_encrypt_decrypt import decrypt, encrypt
from accessor.creditfair import electricity_bill, epf_call_cf
from accessor.pirimid import Pirimid
import pdfkit


def generate_unique_string():
    ts = str(int(datetime.now().timestamp()))
    out = ''
    for i in range(0, len(ts), 2):
        out += str(int(ts[i:i + 2])).encode('utf-8').hex()
    return 'prefix' + out


def validate_age(dob):
    birthdate = datetime.strptime(dob, '%Y-%m-%d')
    today = datetime.today()
    age = today.year - birthdate.year - ((today.month, today.day) < (birthdate.month, birthdate.day))
    if age >= 65:
        return False, "Applicants over 65 years are ineligible for loans. Please change the applicant"
    # if age < 18:
    #     return False, "Applicants below 18 years are ineligible for loans. Please change the applicant"
    return True, None


def validate_ckyc(pan_number, mobile, dob):
    isValidPan = validate_pan_number(pan_number)
    isDobvalid = validate_iso_date(dob)
    isAgeValid, ageErrorMsg = validate_age(dob)
    if mobile:
        isMobileValid = validate_phone(mobile)
    else:
        isMobileValid = True
    if not isValidPan or not isDobvalid or not isAgeValid or not isMobileValid:
        if not isValidPan:
            message = "Please enter a Valid PAN Number"
        elif not isDobvalid:
            message = "Please enter valid DOB"
        elif not isAgeValid:
            message = ageErrorMsg
        else:
            message = "Please enter valid mobile number"
        data = {
            "status": "FAIL",
            "message": message
        }
        return False, 'SF10400', data
    return True, None, None


def no_touch_validate_ckyc(pan_number, dob):
    isValidPan = validate_pan_number(pan_number)
    isDobvalid = validate_iso_date(dob)
    isAgeValid, ageErrorMsg = validate_age(dob)
    if not isValidPan or not isDobvalid or not isAgeValid:
        if not isValidPan:
            message = "Please enter a Valid PAN Number"
        elif not isDobvalid:
            message = "Please enter valid DOB"
        elif not isAgeValid:
            message = ageErrorMsg
        data = {
            "status": "FAIL",
            "message": message
        }
        return False, 'SF10400', data
    return True, None, None


def validate_create_loan_request(request_body):
    is_valid_email = False
    for param in ('first_name', 'last_name', 'address_line_1', 'pincode', 'state', 'city'):
        if param not in request_body:
            message = param + ' not found in request'
            return False, 'SF10400', message
    # if any(k not in request_body for k in ('first_name', 'last_name', 'address_line_1', 'pincode', 'state', 'city')):
    #     return False, 'SF10400'
    if request_body['email']:
        is_valid_email = validate_email(request_body['email'])
    is_valid_fname = validate_name(request_body['first_name'])
    is_valid_lname = validate_name(request_body['last_name'])
    is_valid_pincode = validate_pincode(request_body['pincode'])

    is_valid_state = True if request_body['state'] else False
    is_valid_city = True if request_body['city'] else False

    message_set = {'Please enter valid first name': is_valid_fname,
                   'Please enter valid pincode': is_valid_pincode,
                   'Please enter valid last name': is_valid_lname,
                   'Please enter valid email': is_valid_email,
                   'Please enter valid state': is_valid_state,
                   'Please enter valid city': is_valid_city
                   }
    for key, value in message_set.items():
        if not value:
            message = key
            return False, 'SF10400', message
    return True, None, None


def validate_electricity_bill_request(request_body):
    for param in ('provider', 'customer_id', 'monthly_bill_amount'):
        if param not in request_body:
            message = param + ' not found in request'
            return False, 'SF10400', message
    if not request_body['monthly_bill_amount']:
        message = 'Please enter valid bill amount'
        return False, 'SF10400', message
    # if any(k not in request_body for k in ('provider', 'customer_id', 'monthly_bill_amount')):
    if request_body['provider'] == 'UPPCL' and 'district' not in request_body:
        message = 'District required for UPPCL service provider'
        return False, 'SF10400', message
    return True, None, None


def validate_no_touch_electricity_bill_request(request_body):
    for param in ('provider', 'customer_id', 'monthly_bill_amount'):
        if param not in request_body:
            message = param + ' not found in request'
            return False, 'SF10400', message
    if request_body['provider'] == 'UPPCL' and 'district' not in request_body:
        message = 'District required for UPPCL service provider'
        return False, 'SF10400', message
    return True, None, None


def validate_product_details_request(request_body):
    for param in (
            'application_id', 'project', 'loan_amount', 'kw_installation', 'inverter_to_be_installed',
            'panel_to_be_installed'):
        if param not in request_body:
            message = param + ' not found in request'
            return False, 'SF10400', message
    # if any(k not in request_body for k in ('provider', 'customer_id', 'monthly_bill_amount')):
    if not isinstance(request_body["loan_amount"], int):
        message = 'Please enter valid loan amount'
        return False, 'SF10400', message
    elif not request_body['loan_amount'] > 0 or len(str(request_body["loan_amount"])) > 9:
        message = 'Please enter valid loan amount'
        return False, 'SF10400', message
    if not isinstance(request_body["kw_installation"], int):
        message = 'Please enter valid kw'
        return False, 'SF10400', message
    elif not request_body["kw_installation"] > 0:
        message = 'Please enter valid kw'
        return False, 'SF10400', message
    if len(request_body["inverter_to_be_installed"]) == 0:
        message = 'Please enter Inverter name'
        return False, 'SF10400', message
    if len(request_body["panel_to_be_installed"]) == 0:
        message = 'Please enter Panel name'
        return False, 'SF10400', message
    # if  not isinstance(request_body["subsidy_amount"],int):
    #     message = 'Please enter valid subsidy amount'
    #     return False, 'SF10400', message
    # elif not request_body['subsidy_amount']>0 or len(str(request_body["subsidy_amount"]))>9:
    #     message = 'Please enter valid subsidy amount'
    #     return False, 'SF10400', message
    return True, None, None


def validate_no_touch_product_details_request(request_body):
    for param in (
            'application_id', 'project', 'kw_installation', 'inverter_to_be_installed',
            'panel_to_be_installed'):
        if param not in request_body:
            message = param + ' not found in request'
            return False, 'SF10400', message
    if request_body.get('inverter_to_be_installed'):
        if len(request_body["inverter_to_be_installed"]) == 0:
            message = 'Please enter Inverter name'
            return False, 'SF10400', message
    if request_body.get('panel_to_be_installed'):
        if len(request_body["panel_to_be_installed"]) == 0:
            message = 'Please enter Panel name'
            return False, 'SF10400', message
    return True, None, None


def validate_application_id(app_id):
    if app_id:
        result = str(app_id).isnumeric()
        if result:
            return True, None
    return False, 'Invalid Application ID'


def get_client_ip(request):
    ip = request.remote_addr
    return ip


class EMIService():

    def __init__(self, repository: EMIRepository, utils: Utilities):
        self._repository: EMIRepository = repository
        self._utils: Utilities = utils

    def get_pincode_details(self, pincode):
        if not validate_pincode(pincode):
            return False, 'CF10400', None

        cache_key = f"pincode_data_{pincode}"
        try:
            cached_result = lru_cache.get(cache_key)
            if cached_result:
                return True, 'CF10200', cached_result
        except Exception as e:
            logging.warning(f"Cache get failed for pincode {pincode}: {e}")

        details = self._repository.get_pincode_details(pincode)
        if not details:
            return False, 'CF10400', None

        result = {
            "pincode": pincode,
            "city": details.get('districtname'),
            "state": details.get('statename'),
        }
        try:
            PINCODE_CACHE_TTL = 7776000  # 90 days, matching the legacy pincode cache
            lru_cache.set(cache_key, result, timeout=PINCODE_CACHE_TTL)
        except Exception as e:
            logging.warning(f"Cache set failed for pincode {pincode}: {e}")
        return True, 'CF10200', result
    
    def get_consumer_application_id(self, user_id):
        try:
            application = self._repository.get_consumer_application_id(user_id)
            if application:
                return application.get('id')
            return None
        except CustomExceptions:
            pass
        except Exception as ex:
            CustomExceptions('CF_EXCEPT_02', ex)

    def get_consumer_application_id_by_applicant_id(self, applicant_id):
        try:
            application = self._repository.get_consumer_application_id_by_applicant_id(applicant_id)
            return application.get('id')
        except CustomExceptions:
            pass
        except Exception as ex:
            CustomExceptions('CF_EXCEPT_02', ex)

    def get_application_details_by_mobile(self, mobile_number):
        try:
            application = self._repository.get_application_details_by_mobile(mobile_number)
            return application.get('id')
        except Exception as ex:
            logging.error(ex)
            return None

    def get_products(self, merchant_id: int):
        try:
            logging.debug('into the service factory')
            prod_obj = self._repository.get_product_list(merchant_id)
            if prod_obj:
                logging.debug('Result-set returned successfully')
                prod_list = []
                prod_dict = {}
                for product in prod_obj:
                    prod_dict['id'] = product['id']
                    prod_dict['name'] = product['name']
                    prod_dict['tenure'] = product['warranty']
                    prod_list.append(prod_dict.copy())
                return True, 'CF10200', prod_list
            logging.debug('Result-set empty')
            return False, 'CF11400', None
        except CustomExceptions:
            pass
        except Exception as ex:
            CustomExceptions('CF_EXCEPT_02', ex)

    def add_application(self, first_name, last_name, email, mobile, merchant_id, center_id, product_id, loan_amount,
                        gender, pincode):
        try:
            logging.debug('into the service factory')
            first_name = first_name.rstrip()
            last_name = last_name.rstrip()
            full_name = first_name + ' ' + last_name
            created_at = datetime.now().strftime("%Y-%m-%d, %H:%M:%S")
            valid_check, value = self.create_application_validation(mobile, email, first_name, last_name, merchant_id,
                                                                    center_id)
            logging.info('Validation Status: ' + str(valid_check))
            if not str(loan_amount).isnumeric():
                raise Exception
            if valid_check:
                if type(product_id) is int:
                    product_name, tenure = self._repository.get_product_details(product_id)
                else:
                    product_name = product_id
                    tenure = ''
                vendor_id = request.environ['vendor_id']
                new_customer, application_id = self._repository.create_application(full_name, email, mobile, created_at,
                                                                                   merchant_id, center_id, loan_amount,
                                                                                   gender, pincode,
                                                                                   vendor_id, product_id)
                encoded_app_id = hashids.encode(application_id)
                if new_customer:
                    logging.info('Application created')
                    response_data = {'application_id': encoded_app_id, 'product_name': product_name,
                                     'amount': loan_amount, 'tenure': tenure}
                    # if request.environ['user_type'] == 'merchant':  # merchant directed flow
                    if len(value) > 30 and value[30] != ' ':
                        i = value[:29].rindex(' ')
                        value = value[:i]
                    url = f'{config.FRONT_END_URL}/customer?redirect_url=otp'
                    short_link = create_short_url(url).split('/')[-1]
                    if str(vendor_id) in config.VENDORS:  # vendor specific custom comm
                        state = self._utils.send_sms(mobile, config.JUSPAY_SMS.format(full_name),
                                                     config.JUSPAY_TEMPLATE, application_id)
                        mail_state = self._utils.send_mail(email, config.JUSPAY_MAIL)
                    else:
                        state = self._utils.send_sms(mobile, config.LINK_MSG_BODY.format(' ' + value, short_link),
                                                     config.LINK_TEMPLATE, application_id)  # send link sms to customer
                        mail_state = self._utils.send_mail(email, config.MAIL_MESSAGE.format(' ' + value, short_link))
                    return True, 'CF10200', response_data
                else:
                    logging.info('Duplicate application found: ' + encoded_app_id)
                    return False, 'CF10409', encoded_app_id
            else:
                return False, value, None
        except CustomExceptions:
            pass
        except Exception as ex:
            CustomExceptions('CF_EXCEPT_02', ex)

    def initiate_otp(self, mobile):
        try:
            logging.debug('into the service factory')
            is_valid_number = validate_phone(str(mobile))
            if is_valid_number:
                # check with database if user exists
                is_customer_valid = self._repository.check_user_id_by_mobile(
                    mobile) if request.endpoint == 'emi_api.initiate-otp' else True
                app_id = self._repository.get_application_id(mobile)
                if is_customer_valid:
                    otp = self._utils.generate_otp()
                    logging.debug('otp generated')
                    res = self._utils.send_sms(mobile, config.MSG_BODY.format(otp), config.SMS_TEMPLATE,
                                               app_id)  # send the otp
                    if res:
                        logging.debug('otp sms sent')
                        created_at = datetime.now().strftime("%Y-%m-%d, %H:%M:%S")
                        self._repository.insert_otp(mobile, otp, created_at)
                        return True, 'CF10200',
                else:
                    return False, 'CF14400',
            else:
                return False, 'CF15400',
        except CustomExceptions:
            pass
        except Exception as ex:
            CustomExceptions('CF_EXCEPT_02', ex)

    def send_otp(self, mobile):
        try:
            if mobile == '1437593750':
                return True, 'CF10200', 'Success'
            if is_valid_number:
                cooldown_check = self.cooldown_check(mobile)
                # logging.info(cooldown_check)
                if cooldown_check:
                    logging.info("condition satisfied")
                    otp = self._utils.generate_otp()
                    # logging.info('otp generated')
                    res = self._utils.send_sms(mobile, config.MSG_BODY.format(otp), config.SMS_TEMPLATE,
                                               None)  # send the otp
                    # logging.info(res)
                    if res:
                        logging.debug('otp sms sent')
                        created_at = datetime.now().strftime("%Y-%m-%d, %H:%M:%S")
                        self._repository.insert_otp(mobile, otp, created_at)
                        if self._repository.check_duplicate(mobile_number=mobile):
                            return True, 'CF10200', 'Success'
                        return True, 'CF10200', 'Existing Customer'
                else:
                    return False, "CF14401", None
            else:
                return False, 'CF15400', None
        except Exception as ex:
            bugsnag.notify(ex)
            logging.exception(traceback.print_exc())
            CustomExceptions('CF_EXCEPT_02', ex)

    def send_consent_otp(self, mobile, merchant_id, initiated_from=None, email=None):
        try:
            if mobile == '1437593750':
                return True, 'CF10200', 'Success'
            # merchant_id always takes precedence; only fall back to initiated_from when it is absent
            from_rcp_flow = False
            if not merchant_id and initiated_from:
                merchant = self._repository.get_merchant_by_sales_person_number(initiated_from)
                if not merchant:
                    return False, 'CF10400', 'Please check the entered mobile number. Merchant not found.', None
                merchant_id = merchant.get('id')
                from_rcp_flow = True
            is_valid_number = validate_phone(str(mobile))
            if is_valid_number and merchant_id:
                cooldown_check = self.cooldown_check(mobile)
                if cooldown_check:
                    # skip the application lookup/consent checks when merchant is resolved via the rcp_mobile flow
                    application_id = None
                    if not from_rcp_flow:
                        application_details = self._repository.get_application_details_by_mobile(mobile)
                        if not application_details or application_details.get('merchant_id') != merchant_id:
                            return False, 'CF10400', 'Please check the entered mobile number. Application not found.', None
                        application_id = application_details.get('id')
                        logging.info(f"Application id fetched - {application_id}")
                        element_details = self._repository.get_consumer_element_details(application_id)
                        if element_details.get("is_consent") == 1:
                            return False, 'CF10400', 'Consent already received', None
                        if element_details.get("type_of_customer_application") == 'Non-Individual':
                            return False, 'CF10400', 'Please connect with the advisor for consent of non-individual application', None
                    otp = self._utils.generate_otp()
                    message = f"By validating OTP {otp} I agree to Credit Fair's terms and conditions provided here - https://creditfair.in/privacy-policy, for the loan application and have no objection to them conducting a credit check or contacting me. Team Credit Fair"
                    res, communication_data = self._utils.quick_smart_sms(message, mobile, config.CONSENT_SMS_TEMPLATE, application_id)  # send the otp
                    email_sent = False
                    if email:
                        email_message = f"""
                        <div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; color: #333; line-height: 1.6;">
                            <h2>OTP Verification</h2>
                            <p>Your OTP for the loan application is:</p>
                            <div style="margin: 20px 0; padding: 15px; background-color: #f0f4f8; text-align: center; font-size: 32px; font-weight: bold; border-radius: 8px; letter-spacing: 5px; color: #004b87;">
                                {otp}
                            </div>
                            <p>By validating this OTP, I agree to Credit Fair's <a href="https://creditfair.in/privacy-policy" style="color: #004b87; text-decoration: underline; font-weight: bold;">Privacy Policy</a> and terms and conditions for the loan application, and I have no objection to them conducting a credit check or contacting me.</p>
                            <p style="margin-top: 30px; font-size: 14px; color: #777;">Best regards,<br><b>Team Credit Fair</b></p>
                        </div>
                        """
                        email_sent = self._utils.send_mail(email, email_message)
                    if res == 200 or email_sent:
                        msg_parts = []
                        if email_sent:
                            msg_parts.append(f"email ({email})")
                        if res == 200:
                            msg_parts.append(f"mobile ({mobile})")
                        msg = "OTP sent successfully to " + " and ".join(msg_parts)

                        created_at = datetime.now().strftime("%Y-%m-%d, %H:%M:%S")
                        self._repository.insert_otp(mobile, otp, created_at)
                        self._repository.insert_communication_logs(communication_data)
                        return True, 'CF10200', msg, application_id
                    else:
                        return False, 'CF10400', 'Failed to send OTP via SMS and Email', None
                else:
                    return False, "CF14401", None, None
            else:
                return False, 'CF10400', 'Please provide mobile number relevant to your customer', None
        except Exception as ex:
            logging.exception(traceback.print_exc())

    def verify_consent_otp(self, mobile_number, otp, application_id, latitude, longitude, consent):
        try:
            stored_otp = self._repository.validate_otp(mobile_number, )  # verify the otp
            logging.info('OTP from database: ' + str(stored_otp))
            if stored_otp == int(otp) or otp == (int(config.OTP) if config.OTP else None):
                current_time = datetime.now().strftime("%Y-%m-%d, %H:%M:%S")
                self._repository.update_otp(mobile_number, current_time)
                user_ip = get_client_ip(request)
                consent_type = 'consent_logs' if consent else 'deny_consent_logs'
                if consent:
                    self._repository.save_consent(application_id, latitude=latitude, longitude=longitude, status='success', user_ip=user_ip)
                self.consent_job(application_id, latitude, longitude, consent_type)
                # result = self.generate_consent_document(application_id, latitude, longitude, consent_type)

                return True, 'CF10200', 'Success'
            else:
                return False, 'CF14400', 'Invalid OTP'
        except Exception as ex:
            logging.exception(traceback.print_exc())

    def cooldown_check(self, mobile):
        logging.info("inside cooldown")
        past_time = self._repository.cooldown_time(mobile)
        if past_time is not None:
            current_time = datetime.now()
            difference = current_time - past_time
            return difference > timedelta(minutes=1)
        else:
            return True

    def confirm_otp(self, mobile, otp):
        try:
            logging.debug('into the service factory')
            stored_otp = self._repository.validate_otp(mobile, )  # verify the otp
            logging.info('OTP from database: ' + str(stored_otp))
            if stored_otp == int(otp) or otp == (int(config.OTP) if config.OTP else None):
                logging.info('otp verified successfully')
                current_time = datetime.now().strftime("%Y-%m-%d, %H:%M:%S")
                self._repository.update_otp(mobile, current_time)
                response_dict = {}
                if request.endpoint == 'emi_api.confirm-otp':
                    query_result = self._repository.check_user_id_by_mobile(str(mobile))
                    consumer_application = self._repository.get_consumer_application_id(query_result['id'])
                    consumer_application_element = self._repository.get_consumer_element_details(
                        consumer_application['id'])
                    vendor_id = consumer_application_element.get('vendor_id', 0)
                    if 'HTTP_X_CLIENT_ID' in request.environ:
                        client_id = request.environ['HTTP_X_CLIENT_ID']
                        jwt_secret_key = client_id
                    else:
                        jwt_secret_key = self._repository.get_vendor_api_credentials_id(vendor_id)[1]['key']
                    token = jwt.encode({
                        'iat': datetime.utcnow(),
                        'id': query_result.get('id'),  # consumer application table id for phone
                        'nbf': datetime.utcnow() - timedelta(minutes=10),
                        'exp': datetime.utcnow() + timedelta(days=1),
                        'vendor_id': vendor_id,
                        'token_type': 'access'
                    },
                        jwt_secret_key, algorithm='HS512')
                    logging.debug('token generated')
                    response_dict = {'token': token}
                return True, 'CF10200', response_dict
            else:
                return False, 'CF16400', None
        except CustomExceptions:
            pass
        except Exception as ex:
            CustomExceptions('CF_EXCEPT_02', ex)

    def upload_document(self, cif, doc_name, doc_password, doc_content, doc_format, bank_name, account_type, user_id):
        try:
            logging.debug('into the service factory')
            file_name = config.DOC_PATH[doc_name]
            unhashed_cif = hashids.decode(cif)
            doc_size = (len(doc_content) * 3) / 4

            if doc_size < 10000000 and doc_format.lower() in config.DOC_FORMAT:

                # document_exists = self._repository.check_document(unhashed_cif[0], file_name)
                # if not document_exists:
                s3_uploaded, path = s3_upload(unhashed_cif, file_name, doc_content,
                                              doc_format.lower())  # True, '2258/pan_card.jpeg'
                if s3_uploaded:
                    logging.debug('file uploaded to S3')
                    self._repository.insert_document(unhashed_cif[0], doc_password, file_name, path, bank_name,
                                                     account_type, user_id)
                else:
                    return False, 'CF11409',
            else:
                return False, 'CF10413',

            return True, 'CF10200',
        except CustomExceptions:
            pass
        except Exception as ex:
            CustomExceptions('CF_EXCEPT_02', ex)

    def create_application_validation(self, mobile, email, first_name, last_name, merchant_id, center_id):
        try:
            is_valid_number = validate_phone(str(mobile))
            is_valid_email = validate_email(email)
            is_valid_fname = validate_name(first_name)
            is_valid_lname = validate_name(last_name)
            is_valid_merchant, merchant_name = self._repository.check_merchant_id(merchant_id)
            is_valid_center = self._repository.check_center_id(merchant_id,
                                                               center_id) if center_id is not None else True

            if not is_valid_number or not is_valid_email:
                return False, 'CF18400'
            if not is_valid_lname or not is_valid_fname:
                return False, 'CF12400'
            if not is_valid_center or not is_valid_merchant:
                return False, 'CF11400'
            return True, merchant_name
        except CustomExceptions:
            pass
        except Exception as ex:
            CustomExceptions('CF_EXCEPT_02', ex)

    def update_customer_details(self, hashed_id, pan, dob):
        try:
            is_pan_valid = validate_pan_number(pan)
            is_dob_valid = validate_iso_date(dob)
            app_id = hashids.decode(hashed_id)
            if is_dob_valid and is_pan_valid:
                repo_result = self._repository.update_pan_dob(app_id[0], pan, dob)
                if repo_result:
                    return True, 'CF10200'
            else:
                return False, 'CF00400'
            return False, 'CF10400'
        except CustomExceptions:
            pass
        except Exception as ex:
            CustomExceptions('CF_EXCEPT_02', ex)

    def get_application_status(self, hashed_id):
        try:
            logging.debug('into the service factory')
            app_id = hashids.decode(hashed_id)
            repo_result = self._repository.get_application_details(app_id[0])
            if repo_result:
                state = repo_result['state']
                return True, state, 'CF10200'
            return False, None, 'CF10400'
        except CustomExceptions:
            pass
        except Exception as ex:
            CustomExceptions('CF_EXCEPT_02', ex)

    def application_cancellation_request(self, hashed_id):
        try:
            logging.debug('into the service factory')
            app_id = hashids.decode(hashed_id)[0]
            merchant_id = self._repository.get_merchant_id(app_id)
            adjusted_by = 107
            created_at = datetime.now()
            emi_details = self._repository.get_emi_calculation_details(app_id)
            total_amount_adjusted = emi_details['net_disbursement_amount']
            cancellation_fixed_charges = 0
            cancellation_interest_charges = 0
            total_amount_before_adjusted = total_amount_adjusted - cancellation_fixed_charges
            adjusted_to = None
            repo_result = self._repository.insert_cancellation_details(app_id, merchant_id,
                                                                       cancellation_interest_charges,
                                                                       cancellation_fixed_charges,
                                                                       total_amount_adjusted,
                                                                       total_amount_before_adjusted, adjusted_by,
                                                                       created_at, adjusted_to)
            if repo_result:
                return True, 'CF10200'
            return False, 'CF10400'
        except CustomExceptions:
            pass
        except Exception as ex:
            CustomExceptions('CF_EXCEPT_02', ex)

    def display_merchants(self, category):
        try:
            merchant_dict = {}
            merchants = []
            repo_result = self._repository.get_merchants(category)
            if repo_result:
                for obj in repo_result:
                    merchant_dict['id'] = obj['id']
                    merchant_dict['name'] = obj['merchant_name']
                    if obj['merchant_name'] != "Personal Loan":
                        merchants.append(merchant_dict.copy())
                return True, 'SF10200', list(merchants)
            return False, 'SF10400', None
        except CustomExceptions:
            pass
        except Exception as ex:
            CustomExceptions('CF_EXCEPT_02', ex)

    def digio_vkyc_method(self, application_id, url, kyc_type):
        try:
            if not str(application_id).isnumeric():
                app_id = str(hashids.decode(application_id)[0])
            else:
                app_id = str(application_id)
            check_status, data_row = self._repository.check_digio_vkyc_exists(app_id, 1)
            app_data = self._repository.get_kyc_details(app_id)
            category_id = app_data.get('category_id') if app_data else None
            is_solar = False
            if category_id is not None and str(category_id).isdigit():
                is_solar = int(category_id) in config.SOLAR_CATEGORIES
            if not check_status:
                signer_id = decrypt(app_data['mobile_number'])
                name = app_data['name']
                txn_id = 'CF_' + str(app_id) + '_1_' + str(round(datetime.timestamp(datetime.now())))
                response_data = create_template(signer_id, name, txn_id, app_data.get('lender_id'),
                                                self.get_kyc_param(app_data, 1, is_solar),
                                                kyc_type)
                print(response_data)
                request_id = response_data['access_token']['entity_id']
                gwt_id = response_data['access_token']['id']
                if url:
                    encoded_url = urllib.parse.quote(url, safe='/', encoding=None, errors=None)
                    response_url = config.REDIRECTION_URL + '/#/gateway/login/' + request_id + '/' + txn_id + '/' + signer_id + '?redirect_url=' + encoded_url
                else:
                    response_url = config.REDIRECTION_URL + '/#/gateway/login/' + request_id + '/' + txn_id + '/' + signer_id
                self._repository.insert_kyc_details(app_id, request_id, 1)
                self._repository.insert_digio_vkyc(app_id, request_id, response_url, signer_id, 1)
                return_url = response_url
                kid = request_id
                token = gwt_id
                status = 'incomplete'
            else:
                print(data_row)
                return_url = data_row['kyc_request_url']
                kid = data_row['request_id']
                token = None
                status = 'complete' if data_row['kyc_journey'] == 'complete' else 'incomplete'
            if app_data['co_applicant_name'] and app_data['co_applicant_mobile_number']:
                logging.info("Inside the co-applicant block")
                co_app_check_status, co_app_data_row = self._repository.check_digio_vkyc_exists(app_id, 2)
                if not co_app_check_status:
                    co_app_signer_id = decrypt(app_data['co_applicant_mobile_number'])
                    name = app_data['co_applicant_name']
                    co_app_txn_id = 'CF_' + str(app_id) + '_2_' + str(round(datetime.timestamp(datetime.now())))
                    co_app_response_data = create_template(co_app_signer_id, name, co_app_txn_id,
                                                           app_data.get('lender_id'),
                                                           self.get_kyc_param(app_data, 2, is_solar),
                                                           kyc_type)
                    co_app_request_id = co_app_response_data['access_token']['entity_id']
                    co_app_gwt_id = co_app_response_data['access_token']['id']
                    co_app_response_url = config.REDIRECTION_URL + '/#/gateway/login/' + co_app_request_id + '/' + co_app_txn_id + '/' + co_app_signer_id
                    self._repository.insert_kyc_details(app_id, co_app_request_id, 2)
                    self._repository.insert_digio_vkyc(app_id, co_app_request_id, co_app_response_url, co_app_signer_id,
                                                       2)
                    co_app_return_url = co_app_response_url
                    co_app_kid = co_app_request_id
                    co_app_token = co_app_gwt_id
                    co_app_status = 'incomplete'
                else:
                    co_app_return_url = co_app_data_row['kyc_request_url']
                    co_app_kid = co_app_data_row['request_id']
                    co_app_token = None
                    co_app_status = 'complete' if co_app_data_row['kyc_journey'] == 'complete' else 'incomplete'
            else:
                return_url = data_row['kyc_request_url'] if data_row else return_url
                kid = data_row['request_id'] if data_row else kid
                token = None
                co_app_return_url = None
                co_app_kid = None
                co_app_token = None
                co_app_status = None
            data = {
                'applicant': {
                    'kyc_url': create_short_url(return_url),
                    'kid': kid,
                    'token': token,
                    'status': status
                },
                'co_applicant': {
                    'kyc_url': create_short_url(co_app_return_url),
                    'kid': co_app_kid,
                    'token': co_app_token,
                    'status': co_app_status
                }
            }
            return 'CF10200', data
        except CustomExceptions:
            pass
        except Exception as ex:
            logging.exception(traceback.print_exc())
            CustomExceptions('CF_EXCEPT_02', ex)

    def get_kyc_param(self, app_data, applicant_type, is_solar=True):
        father_name = app_data.get('father_name')
        if applicant_type == 2:
            father_name = app_data.get('co_applicant_father_name')

        email = app_data.get('email')
        if applicant_type == 2 and app_data.get('co_applicant_email') is not None:
            email = app_data.get('co_applicant_email')

        loan_amount = app_data.get("net_loan_amount") or app_data.get("approved_amount", 0)
        emi_amount = app_data.get("emi_amount", 0)
        tenure = app_data.get("tenure", 0)

        if not is_solar:
            emi_display = f"{emi_amount} / {loan_amount} / {tenure} Months" if tenure else f"{emi_amount} / {loan_amount}"
        else:
            emi_display = emi_amount

        param = {
            "father_name": father_name,
            "email_id": decrypt(email) if email else None,
            "current_location_and_city": app_data.get("current_location_and_city"),
            "loan_purpose": app_data.get("loan_purpose"),
            "loan_amount": loan_amount,
            "emi_amount": emi_display,
            "is_solar": is_solar,
        }

        if is_solar:
            param["installation_address"] = app_data.get("installation_address")

        return param

    def digio_generate_token(self, application_id, app_type):
        try:
            if not str(application_id).isnumeric():
                app_id = str(hashids.decode(application_id)[0])
            else:
                app_id = str(application_id)
            check_status, data_row = self._repository.check_digio_vkyc_exists(app_id, app_type)
            app_data = self._repository.get_application_details(app_id)
            if check_status:
                response_data = regenerate_token(data_row['request_id'], app_data.get('lender_id'))
                token = response_data['response']['id'] if 'id' in response_data['response'] else 'Null'
                data = {
                    'token': token
                }
                return 'CF10200', data
            return 'CF12409', None
        except CustomExceptions:
            pass
        except Exception as ex:
            CustomExceptions('CF_EXCEPT_02', ex)

    def ckyc(self, pan_number, dob, mobile, merchant_id, center_id, unique_id, updated_by):
        try:
            # if not merchant_id:
            #     merchant_id = config.MNRE_MERCHANT
            pan_number = pan_number.upper()
            category_id = config.SOLAR_CATEGORY_ID
            is_data_valid, code, data = validate_ckyc(pan_number, mobile, dob)
            # isValidPan = validate_pan_number(pan_number)
            # isValidMobile = validate_phone(mobile)
            # isDobvalid = validate_iso_date(dob)
            # if not isValidPan or not isValidMobile or not isDobvalid:
            #     if not isValidPan:
            #         message = "Please enter a Valid PAN Number"
            #     elif not isValidMobile:
            #         message = "Please enter valid Mobile number"
            #     else:
            #         message="Please enter valid DOB"
            #     data = {
            #         "status": "FAIL",
            #         "message": message,
            #     }
            #     return 'SF10400', data
            if is_data_valid:
                random_id = generate_unique_string()
                print("Random Unique String with Prefix:", random_id)
                check_duplicate = self._repository.check_duplicate(mobile_number=mobile, pan_card=pan_number)
                if check_duplicate:
                    response = creditfair.callckyc(pan_number, dob, random_id, mobile)

                    if response["status"] == "OK" and response["kycStatus"] == "SUCCESS":
                        # return response
                        data = response
                        response2 = response["data"]
                        response2["message"] = response["message"]
                        data["data"]["merchant_id"] = merchant_id
                        data["data"]["category_id"] = category_id
                    else:
                        data = {
                            "status": "FAIL",
                            "message": "Something went wrong",
                            "data": {
                                "pan_number": pan_number,
                                "dob": dob,
                                "mobile_number": mobile,
                                "merchant_id": merchant_id,
                                "category_id": category_id
                            }
                        }
                        response2 = {"message": response["message"]}
                    check_status, app_id = self._repository.insert_ckyc(data, center_id, unique_id, updated_by)
                    if check_status:
                        logging.info("successfully created application")
                        logging.info(id)
                        response2["application_id"] = app_id
                    else:
                        logging.info(id)
                    return 'CF10200', response2
                else:
                    return 'CF12409', None
            else:
                return code, data
        except CustomExceptions:
            pass
        except Exception as ex:
            CustomExceptions('CF_EXCEPT_02', ex)

    def employement_details(self, application_id, mobile, employement_type, net_monthyl_income, company_name,
                            gst_number):
        try:
            if employement_type != 'Unemployed':
                if len(str(employement_type)) > 0 and net_monthyl_income > 0 and len(str(application_id)) > 0 and len(
                        str(net_monthyl_income)) <= 9:
                    if mobile != None:
                        is_no_valid = validate_phone(str(mobile))
                        if is_no_valid:
                            mobile = encrypt(str(mobile))
                        else:
                            logging.info("entered invalid mobile no")
                            return False, "CF16400", "Office contact number is not correct"
                else:
                    msg = "error"
                    if net_monthyl_income < 0 or len(str(net_monthyl_income)) >= 9:
                        msg = "Please Enter Valid Income"
                    return False, "CF16400", msg
            name, consumer_mobile, consumer_email, merchant_id = self._repository.consumer_name(application_id)
            consumer_mobile = decrypt(str(consumer_mobile))
            consumer_email = decrypt(str(consumer_email))
            if employement_type.lower() == "SALARIED".lower() and merchant_id != 73:
                epf = epf_call_cf(company_name, name, consumer_mobile, consumer_email, application_id)
            company_id = self._repository.check_company_present(company_name)
            if not company_id and employement_type.lower() != 'Unemployed'.lower():
                company_id = self._repository.insert_company(company_name)
            else:
                company_id = None
            self._repository.update_employment_details(application_id, mobile, employement_type, net_monthyl_income,
                                                       company_id, gst_number)
            self._repository.update_application_state(consumer_mobile, "in-process", application_id)
            return True, "CF10200", "saved"
        except Exception as ex:
            CustomExceptions('CF_EXCEPT_02', ex)

    def create_loan(self, unique_id, first_name, middle_name, last_name, email, mobile_number, address_line_1,
                    address_line_2,
                    address_line_3, pincode, app_id, city, state, center_id):
        try:
            name = first_name + ' ' + middle_name + ' ' + last_name
            full_address = address_line_1 + ' ' + address_line_2
            status, application_id = self._repository.add_solar_application(unique_id, name, email, mobile_number,
                                                                            full_address,
                                                                            address_line_3, pincode, app_id, state,
                                                                            city, center_id)
            response_data = {'application_id': application_id}
            if status:
                application_data = self._repository.get_application_details(app_id)
                # response = fetch_bureau(app_id, 1, first_name, last_name, decrypt(application_data['pan_card']), application_data['dob'].strftime('%Y-%m-%d'),
                #              decrypt(application_data['mobile_number']), full_address, city, state, pincode, 100000)
                # logging.info(response.text)
                return True, 'SF10200', response_data
            else:
                return False, 'SF10409', response_data
        except CustomExceptions:
            pass
        except Exception as ex:
            CustomExceptions('CF_EXCEPT_02', ex)

    def get_electricity_bill_details(self, app_id, provider, customer_id, district, monthly_bill,
                                     mobile_number, other_provider):
        try:
            if provider == 'OTH':
                self._repository.update_monthly_bill(monthly_bill, app_id)
                status = {'message': 'Other Service Provider Selected'}
            else:
                status = electricity_bill(app_id, customer_id, provider, district, mobile_number)
                logging.info(status)
            if status['message'] in ('Successfully validated utility bill', 'Other Service Provider Selected'):
                provider_name = config.inverted_dict.get(provider)
                self._repository.update_provider_name(provider_name, other_provider, app_id)
                name, consumer_mobile, consumer_email, merchant_id = self._repository.consumer_name(app_id)
                # self._repository.update_application_state(decrypt(consumer_mobile), "waiting for decision", app_id)
                return True, 'SF10200'
            elif status['message'] == 'Failed to validate utility bill':
                return False, 'SF10200'
        except CustomExceptions:
            pass
        except Exception as ex:
            CustomExceptions('CF_EXCEPT_02', ex)

    def process_product_details(self, app_id, project, loan_amount, kw_installation, inverter_to_be_installed,
                                panel_to_be_installed, subsidy):
        try:
            status = self._repository.save_product_details(app_id, project, loan_amount, kw_installation,
                                                           inverter_to_be_installed, panel_to_be_installed, subsidy)
            if status:
                auto_decision_response = creditfair.run_auto_decision(app_id)
                if auto_decision_response["message"] == "Approved":
                    get_offers = creditfair.get_offers(app_id)

                    if get_offers["status-code"] == 1:
                        auto_decision_response['data'] = get_offers["data"]['emi_summary']
                    else:
                        auto_decision_response['data'] = get_offers["status-message"]
                return auto_decision_response, 'SF10200'
            else:
                return None, 'SF10409'
        except CustomExceptions:
            pass
        except Exception as ex:
            CustomExceptions('CF_EXCEPT_02', ex)

    @staticmethod
    def get_schema_file():
        try:
            body = s3_read_file('flutter/solar_schema/solar_schema.json')
            # with open('./emi-checkout/solar_schema.json', 'r') as body:
            #     body = body.read()
            schema = json.loads(body)
            return schema
        except Exception as ex:
            logging.exception("An error occurred")

    @staticmethod
    def get_no_touch_config():
        try:
            body = s3_read_file('flutter/no_touch/web_app.json')
            # with open('./emi-checkout/web_app.json', 'r') as body:
            # with open('web_app.json', 'r') as body:
            #     body = body.read()
            schema = json.loads(body)
            return schema
        except Exception as ex:
            logging.exception("An error occurred")

    @staticmethod
    def get_consumer_app_schema_file():
        try:
            body = s3_read_file('flutter/consumer_app/consumer_app.json')
            # with open('./emi-checkout/consumer_app.json', 'r') as body:
            # with open('consumer_app.json', 'r') as body:
            #     body = body.read()
            schema = json.loads(body)
            return schema
        except Exception as ex:
            logging.exception("An error occurred")

    @staticmethod
    def get_inverters_list():
        try:
            data = ['TATA Power Solar', 'Waaree', 'Mundra Solar PV (Adani Solar)', 'Renewsys India', 'Emmvee PV Power',
                    'Premier Energies', 'Pixon Green Energy', 'Websol Energy', 'Havells', 'Solarium', 'Navitas Solar',
                    'Jakson Engineers Ltd', 'M/s Sova Solar Ltd', 'Pixon Green Energy', 'Websol Energy Systems',
                    'Alpex Solar Pvt. Ltd', 'M/s Novus Green Energy', 'PV Power Technologies', 'M/s Rayzon Green',
                    'M/s Kosol Energies Pvt.', 'Gautam Solar Pvt. Ltd', 'M/s Australian Premium Solar',
                    'Topsun Energy Ltd', 'Navitas Green Solutions', 'Livguard', 'Polycab','Sungrow','ZunSolar','Vsole Solar','Deye','KSolare','Feston','Solis' ,'Other']
            return data
        except CustomExceptions:
            pass
        except Exception as ex:
            CustomExceptions('CF_EXCEPT_02', ex)

    @staticmethod
    def get_panel_list():
        try:
            data = ['TATA Power Solar', 'Waaree', 'Mundra Solar PV (Adani Solar)', 'Renewsys India', 'Emmvee PV Power',
                    'Premier Energies', 'Pixon Green Energy', 'Websol Energy', 'Havells', 'Solarium', 'Navitas Solar',
                    'Jakson Engineers Ltd', 'M/s Sova Solar Ltd', 'Pixon Green Energy', 'Websol Energy Systems',
                    'Alpex Solar Pvt. Ltd', 'M/s Novus Green Energy', 'PV Power Technologies', 'M/s Rayzon Green',
                    'M/s Kosol Energies Pvt.', 'Gautam Solar Pvt. Ltd', 'M/s Australian Premium Solar',
                    'Topsun Energy Ltd', 'Navitas Green Solutions', 'Livguard', 'Polycab', 'Saatvik','Adani','ZunSolar','Intersolar','Senza','Ultima','Renew Power Private Limited','Loom solar','JAKSON BIFACIAL','Other']
            return data
        except Exception as ex:
            logging.exception("An error occurred")

    def save_offer(self, app_id, offer_id):
        try:
            status = self._repository.save_offer(app_id, offer_id)
            if status:
                result = self._repository.update_application_state(None, 'approved', app_id)
                return True, 'SF10200'
            return False, 'SF10400'
        except Exception as ex:
            logging.exception("An error occurred")

    def display_merchant_name(self, merchant_slug):
        try:
            status = self._repository.get_merchant_name(merchant_slug)
            if status:
                return True, 'SF10200', status
            return False, 'SF10400', None
        except Exception as ex:
            logging.exception("An error occurred")

    def display_merchant_center_name(self, merchant_center_slug):
        try:
            status = self._repository.get_merchant_center_name(merchant_center_slug)
            if status:
                return True, 'SF10200', status
            return False, 'SF10400', None
        except Exception as ex:
            logging.exception("An error occurred")

    def get_bureau(self, app_id, first_name, last_name, pan_card, mobile_number, address_line_1, address_line_2,
                   address_line_3, pincode, city, state, dob):
        try:
            full_address = address_line_1 + ' ' + address_line_2
            # response = fetch_bureau(app_id, 1, first_name, last_name, pan_card, dob,
            #            mobile_number, full_address, city, state, pincode, 100000)
            # logging.info(response.text)
            data = {
                "application_id": str(app_id),
                "applicant_type": 1,
                "request_type": "customer",
                "first_name": first_name,
                "last_name": last_name,
                "pan_card": pan_card,
                "document_type": "PAN_DETAILED",
                "consent": "Y",
                "mobile_number": mobile_number,
                "gender": "1",
                "dob": dob,
                "address": full_address,
                "state": state,
                "city": city,
                "postal_code": pincode,
                "loan_amount": 100000,
                "vendor": "partner link"
            }
            payload = {
                "cmd": "command:SolarBureauCall",
                "con": "jobs",
                "param": data,
                "job_handler": "App\\Helpers\\CmdHelper@runCommand"
            }
            self._repository.insert_to_queue(json.dumps(payload), app_id, "SolarBureauCall")
            return True, 'SF10200'
        except Exception as ex:
            logging.exception("An error occurred")

    def get_user_journey_details(self, mobile):
        try:
            app_id = self._repository.get_application_id(mobile)
            if app_id:
                application_details = self._repository.get_application_details(app_id)
                name = application_details.get('name') if application_details.get('name') else ''
                name_split = name.split(' ') if name else ''
                first_name = name_split[0] if len(name_split) > 0 else ''
                last_name = name_split[-1] if len(name_split) > 1 else ''
                # mobile_number = decrypt(application_details.get('mobile_number'))
                email = decrypt(application_details.get('email')) if application_details.get('email') else ''
                pan_number = decrypt(application_details.get('pan_card'))
                dob = application_details.get('dob').strftime('%Y-%m-%d')
                addr_1 = application_details.get('current_address_line_1') if application_details.get(
                    'current_address_line_1') else ''
                # addr_2 = application_details.get('current_address_city') if application_details.get('current_address_city') else ''
                if addr_1:
                    addr_list = addr_1.split(' ')
                    addr_1 = " ".join(addr_list[:4])
                    addr_2 = " ".join(addr_list[4:8])
                    addr_3 = " ".join(addr_list[8:])
                else:
                    addr_1, addr_2, addr_3 = '', '', ''
                pincode = application_details.get('permanent_address_pincode') if application_details.get(
                    'permanent_address_pincode') else ''
                city = application_details.get('permanent_address_city') if application_details.get(
                    'permanent_address_city') else ''
                state = application_details.get('permanent_address_state') if application_details.get(
                    'permanent_address_state') else ''
                application_element_details = self._repository.get_consumer_element_details(app_id)
                ckyc_status = True
                basic_details = True if application_details.get('user_id') else False
                utility_bill_status = True if application_element_details.get('inverter_monthly_power_bill') else False
                employment_status = True if application_details.get('employee_status') else False
                approval_log_data = self._repository.get_approval_status_log(app_id)
                bre_status = True if approval_log_data else False
                if application_details.get('offer_id') or bre_status:
                    plan_offer_saved = True
                else:
                    plan_offer_saved = False
                status_dict = {
                    'ckyc': {'status': ckyc_status, 'section': 0, 'page': 0},
                    'application_details': {'status': basic_details, 'section': 0, 'page': 2},
                    'employment_details': {'status': employment_status, 'section': 1, 'page': 0},
                    'electricity_bill_validation': {'status': utility_bill_status, 'section': 2, 'page': 0},
                    'product_details': {'status': bre_status, 'section': 3, 'page': 0},
                    'plan_selected': {'status': plan_offer_saved, 'section': 4, 'page': 0}
                }
                show_decision, section, page = self.get_section_page(status_dict)
                if show_decision:
                    decision_path = self.get_decision_value(application_details.get('state'))
                else:
                    decision_path = None
                response_data = {
                    'application_id': app_id,
                    'merchant_id': application_details.get('merchant_id'),
                    'center_id': application_details.get('merchant_location_id'),
                    'section': section,
                    'page': page,
                    'basic_details': {
                        'first_name': first_name,
                        'last_name': last_name,
                        'mobile_number': mobile,
                        'email': email,
                        'addr1': addr_1,
                        'addr2': addr_2,
                        'addr3': addr_3,
                        'pincode': pincode,
                        'city': city,
                        'state': state,
                        'dob': dob,
                        'pan_number': pan_number
                    },
                    'decision': {
                        'status': show_decision,
                        'value': decision_path
                    }
                }
                return True, response_data, 'SF10200'
            else:
                response_data = None
                return False, response_data, 'SF10200'
        except Exception as ex:
            logging.exception("An error occurred")

    @staticmethod
    def get_section_page(dict):
        for k, v in dict.items():
            if not v.get('status'):
                return False, v.get('section'), v.get('page')
        return True, None, None

    @staticmethod
    def get_decision_value(state):
        if state == 'waiting for decision':
            return '/onboarding/not-approved'
        elif state == 'cf approved':
            return '/onboarding/approved'
        elif state == 'rejected':
            return '/onboarding/auto-reject'

    # ******************************** NO TOUCH ***************************************
    @staticmethod
    def get_no_touch_decision_value(state, acct_aggr_status, bureau_page):
        if bureau_page:
            return '/onboarding/bureau-failure'
        if state == 'waiting for decision':
            if acct_aggr_status:
                return '/onboarding/not-approved'
            else:
                return '/onboarding/bank-statement'
        elif state == 'approved':
            return '/onboarding/final-success'
        elif state == 'rejected':
            return '/onboarding/auto-reject'

    def create_application(self, mobile_number, pan_number, date_of_birth, unique_id, loan_amount, merchant_id,
                           center_id, product, source, origin=None):
        valid_request, code, data = validate_ckyc(pan_number, None, date_of_birth)
        is_valid_merchant, merchant_name = self._repository.check_merchant_id(merchant_id)
        is_valid_center = self._repository.check_center_id(merchant_id,
                                                           center_id) if center_id is not None else True
        is_valid_mobile = validate_phone(str(mobile_number))
        if not is_valid_mobile:
            return False, None, 'SF10400', 'Please enter valid mobile'
        if not is_valid_merchant:
            return False, None, 'SF10400', 'Please enter valid merchant'
        if product is None:
            return False, None, 'SF10400', 'Please select product '
        if not is_valid_center:
            return False, None, 'SF10400', 'Please enter valid center'
        if int(loan_amount) > 2000000:
            return False, None, 'SF10400', 'The entered amount is more than the maximum amount allowed'
        elif int(loan_amount) < 10000:
            return False, None, 'SF10400', 'The entered amount is less than the minimum amount allowed'
        if not valid_request:
            return valid_request, None, code, data.get('message')
        pan_dedupe = self._repository.check_duplicate(pan_card=pan_number, mobile_number=mobile_number)
        if pan_dedupe:
            # api_result = pan_dob_status(pan_number)
            # api_response = api_result.json()
            # if api_result.status_code != 200 or api_response.get('result').get('status') != 'Active':
            #     return False, None, 'SF10400', 'Please enter valid PAN'
            # if api_response.get('result').get('dob') != date_of_birth:
            #     return False, None, 'SF10400', 'Please input details as per PAN'
            status, app_id, user_id = self._repository.create_loan_pan_dob(mobile_number, pan_number, date_of_birth,
                                                                           unique_id,
                                                                           loan_amount,
                                                                           merchant_id, center_id, product, source)
            token = jwt.encode({
                'iat': datetime.utcnow(),
                'id': user_id,  # consumer application table id for phone
                'nbf': datetime.utcnow() - timedelta(minutes=10),
                'exp': datetime.utcnow() + timedelta(days=1),
                'token_type': 'access'
            }, config.SECRET_KEY, algorithm='HS512')
            logging.info(token)
            data = {'application_id': app_id, 'token': token}
            communication_send = self.send_create_application_communication(merchant_name, mobile_number, app_id, origin)
            return True, data, 'SF10200', 'Loan Application Created'
        return False, None, 'SF10400', 'Customer already exists'

    def send_create_application_communication(self, value, mobile, application_id, origin=None):
        try:
            if origin is not None:
                if len(value) > 30 and value[30] != ' ':
                    i = value[:29].rindex(' ')
                    value = value[:i]
                url = f'{origin}/onboarding'
                short_link = create_short_url(url).split('/')[-1]
                state = self._utils.send_sms(mobile, config.LINK_MSG_BODY.format(' ' + value, short_link),
                                             config.LINK_TEMPLATE, application_id)  # send link sms to customer
                return True
            else:
                return True
        except Exception as ex:
            logging.info(ex)
            logging.info("communication not send")
            return True

    def update_application(self, request_body):
        valid_app_id, response_text = validate_application_id(request_body.get('application_id'))
        if request_body:
            valid_email = validate_email(request_body.get('email'))
            if not valid_app_id:
                return False, 'SF10400', response_text
            if not valid_email:
                return False, 'SF10400', 'Please enter valid email ID'
            if request_body.get("gender") is None:
                return False, 'SF10400', 'Please select gender'
            valid_first_name = validate_name(request_body.get('first_name'))
            if not valid_first_name:
                return False, 'SF10400', 'Please enter valid first name'
            valid_father_name = validate_name(request_body.get('father_name'))
            if not valid_father_name:
                return False, 'SF10400', 'Please enter valid father name'
            valid_last_name = validate_name(request_body.get('last_name'))
            if not valid_last_name:
                return False, 'SF10400', 'Please enter valid last name'
            valid_pincode = validate_pincode(request_body.get('pincode'))
            if not valid_pincode:
                return False, 'SF10400', 'Please enter valid pincode'

            status, response = self._repository.dedupe_update_application(request_body)
            if status:
                return True, 'SF10200', None
            return False, 'SF10400', 'Email already exists'

    def display_merchant_category(self, merchant_id):
        try:
            repo_result = self._repository.get_merchant_category(merchant_id)
            if repo_result:
                return True, 'SF10200', repo_result
            return False, 'SF10400', None
        except Exception as ex:
            logging.exception("An error occurred")

    def fetch_presigned_url(self, file_path, content_type, file_name, application_id):
        try:
            url = upload_presigned_url(file_path, 3600, content_type)
            return True, 'SF10200', url
        except Exception as ex:
            logging.exception("An error occurred")

    def file_path(self, file_path, file_name):
        file_extension = file_path.split('.')[1]
        application_id = file_path.split("/")[0]
        file_name = application_id + "/" + f"{file_name}." + file_extension
        return file_name, application_id

    def upload_document_entry(self, documents):
        try:
            for file in documents.keys():
                owner = 0
                bank_statement_pass = None
                if documents.get(file) is None or file in ["owner", "application_id", "bank_statement_password",
                                                           "merchant_sse", "bank_account_type", "bank_name"]:
                    continue
                if file == 'other_document':
                    image_path = documents.get(file).get('path')
                    if image_path is None:
                        continue
                    file = 'other_document' if not documents.get(file).get('tag', 'other_document') else documents.get(
                        file).get('tag', 'other_document')
                    file_path, application_id = self.file_path(image_path, 'other_document')
                else:
                    image_path = documents[file]
                    file_path, application_id = self.file_path(image_path, file)
                    if file == "electricity_bill_photo" and documents.get("electricity_bill_photo") is not None:
                        if documents.get("owner") == "yes":
                            owner = 1
                        else:
                            owner = 0
                    if file == "bank_statement":
                        bank_statement_pass = documents.get("bank_statement_password")
                        file_content = s3_read_file(file_path)
                        application_details = self._repository.get_application_details(application_id)
                        user_id = application_details.get('created_by') if application_details.get('created_by') else None
                        ist = pytz.timezone("Asia/Kolkata")
                        timestamp = datetime.now(ist).strftime("%Y-%m-%d, %H:%M:%S")
                        message = "Bank statement uploaded successfully by consumer via one.creditfair.in link"
                        if user_id:
                            user_name = self._repository.get_user_name_by_id(user_id)
                            comment = f"@{user_name} {message}"
                        else:
                            comment = message
                        self._repository.insert_comment(application_id, 107, comment, 0, 1, timestamp, timestamp, "consumer")
                        bank_analysis_count = self._repository.get_pirimid_analysis_count(application_id)
                        if bank_analysis_count < 3:
                            data = {'file': file_content, 'password': bank_statement_pass,
                                    'id': application_id, 'employment_type': application_details.get('employment_type'),
                                    'applicant_type': documents.get('applicant_type') if documents.get('applicant_type') else 1}
                            response = Pirimid().initiate_bank_statement_analysis(data, self._repository)
                            logging.info(response)
                        else:
                            logging.info(f"Pirimid analysis limit reached for app {application_id}")
                        self._trigger_webhook_event(application_id, 'Bank-Statement-Uploaded')
                self._repository.insert_document(application_id, bank_statement_pass, file, file_path,
                                                 None, None, owner)
            return True, 'SF10200'
        except Exception as ex:
            logging.exception("An error occurred")

    def _trigger_webhook_event(self, application_id, event):
        try:
            merchant_id = self._repository.get_merchant_id_by_app_id(application_id)
            if not merchant_id:
                return
            webhook = self._repository.get_webhook_by_merchant_id(merchant_id)
            if not webhook:
                return
            event_list = webhook.get('events')
            if event_list:
                if isinstance(event_list, str):
                    try:
                        event_list = json.loads(event_list)
                    except (json.JSONDecodeError, ValueError):
                        event_list = [e.strip() for e in event_list.split(',')]
            if not event_list or event not in event_list:
                return
            loan_application_no = self._repository.get_loan_application_no(application_id)
            payload = {
                "cmd": "command:mConnect",
                "con": "mConnectWebhook",
                "param": {
                    "webhook_id": json.dumps([webhook.get('id')]),
                    "application_id": str(application_id),
                    "payload": {
                        "returnCode": "000000",
                        "applicationNo": str(loan_application_no) if loan_application_no else "",
                        "type": event,
                        "status": "success",
                        "data": {}
                    }
                },
                "job_handler": "App\\Helpers\\CmdHelper@runCommand"
            }
            self._repository.insert_to_queue(json.dumps(payload), application_id, "mConnectWebhook")
        except Exception as ex:
            logging.exception("An error occurred while triggering webhook event")

    def store_installation_address(self, request_body):
        valid_app_id, response_text = validate_application_id(request_body.get('application_id'))
        if request_body:
            if not valid_app_id:
                return False, 'SF10400', response_text
            valid_pincode = validate_pincode(request_body.get('pincode'))
            if not valid_pincode:
                return False, 'SF10400', 'Please enter valid pincode'
            status, response = self._repository.store_installation_address(request_body)
            if status:
                return True, 'SF10200', None
            return False, 'SF10400', 'Email already exists'

    def get_available_offers(self, application_id):
        offers = creditfair.get_offers(application_id)
        product_name = self._repository.get_product_name(application_id)
        for item in offers.get('data').get('emi_summary'):
            item['product'] = product_name
        return True, 'CF10200', offers.get('data').get('emi_summary')

    def no_touch_confirm_otp(self, mobile, otp, consumer_app):
        try:
            applicant_id = user_id = None
            existing_customer = 0
            is_valid_number = validate_phone(str(mobile))
            if len(str(otp)) != 6:
                return False, 'CF16400', None, 'Please enter valid otp'
            if not is_valid_number:
                return False, 'CF15400', None, 'Please enter valid mobile number'
            logging.debug('into the service factory')
            stored_otp = self._repository.validate_otp(mobile, )  # verify the otp
            logging.info('OTP from database: ' + str(stored_otp))
            if (stored_otp == int(otp) or otp == (int(config.OTP) if mobile == 1437593750 else None) or
                    otp == (int(111111) if mobile == 9920527883 else None)):
                logging.info('otp verified successfully')
                current_time = datetime.now().strftime("%Y-%m-%d, %H:%M:%S")
                self._repository.update_otp(mobile, current_time)
                merchant_id = self._repository.get_merchant_from_mobile(mobile)
                application_details = self._repository.get_application_details_by_mobile(mobile)
                if merchant_id:
                    return False, 'CF10400', None, 'Please use the merchant admin panel for now'
                if consumer_app:
                    duplicate = self._repository.check_duplicate(mobile_number=mobile, days=None)
                    if not duplicate:
                        if application_details.get('updated_by') != 'consumer_app' and application_details.get('state') not in ['disbursed', 'nach registered']:
                            return False, 'CF10400', None, 'Please contact the advisor - 8928769161'
                        else:
                            existing_customer = 1
                else:
                    duplicate = self._repository.check_duplicate(mobile_number=mobile)
                    if not duplicate:
                        if application_details.get('updated_by') not in ['No touch', "Partner Link"]:
                            return False, 'CF10400', None, 'Please contact the advisor - 8928769161'
                        else:
                            existing_customer = 1
                if existing_customer == 1:
                    if self._repository.check_user_id_by_mobile(mobile):
                        user_id = self._repository.check_user_id_by_mobile(mobile).get('id')
                    else:
                        applicant_id = application_details.get("applicant_id")
                    token = jwt.encode({
                        'iat': datetime.utcnow(),
                        'id': user_id,  # consumer application table id for phone
                        'applicant_id': applicant_id,
                        'nbf': datetime.utcnow() - timedelta(minutes=10),
                        'exp': datetime.utcnow() + timedelta(days=1),  # set to days=1
                        'token_type': 'access'
                    }, config.SECRET_KEY, algorithm='HS512')
                    logging.info(token)
                    return True, 'CF10200', {'application_id': application_details.get('id'),
                                             'token': token}, 'Existing Customer'
                else:
                    token = jwt.encode({
                        'iat': datetime.utcnow(),
                        'mobile_number': mobile,  # consumer application table id for phone
                        'nbf': datetime.utcnow() - timedelta(minutes=10),
                        'exp': datetime.utcnow() + timedelta(minutes=10),
                        'token_type': 'temporary'
                    }, config.SECRET_KEY, algorithm='HS512')
                    logging.info(token)
                    return True, 'CF10200', {'token': token}, 'Successful OTP verification'
            else:
                return False, 'CF16400', None, 'Incorrect OTP. Please enter correct OTP.'
        except Exception as ex:
            logging.exception("An error occurred")

    def get_token(self, application_id, type):
        try:
            application_details = self._repository.get_application_details(application_id)
            if application_details:
                if type == 'temporary':
                    token = jwt.encode({
                        'iat': datetime.utcnow(),
                        'mobile_number': decrypt(application_details.get('mobile_number')),  # consumer application table id for phone
                        'nbf': datetime.utcnow() - timedelta(minutes=10),
                        'exp': datetime.utcnow() + timedelta(minutes=10),
                        'token_type': 'temporary'
                    }, config.SECRET_KEY, algorithm='HS512')
                else:
                    user_id = application_details.get("user_id")
                    token = jwt.encode({
                        'iat': datetime.utcnow(),
                        'id': user_id,  # consumer application table id for phone
                        'nbf': datetime.utcnow() - timedelta(minutes=10),
                        'exp': datetime.utcnow() + timedelta(days=1),
                        'token_type': 'access'
                    }, config.SECRET_KEY, algorithm='HS512')
                logging.info(token)
                return True, 'CF10200', {'token': token}, 'Token generated successfully'
            else:
                return False, 'CF16400', None, 'Incorrect Application ID.'
        except Exception as ex:
            logging.exception("An error occurred")

    def no_touch_employment_details(self, application_id, mobile, employement_type, net_monthyl_income, company_name,
                                     gst_number):
        try:
            if employement_type != 'Unemployed':
                if len(str(employement_type)) > 0 and net_monthyl_income > 0 and len(str(application_id)) > 0 and len(
                        str(net_monthyl_income)) <= 9:
                    if mobile is not None:
                        is_no_valid = validate_phone(str(mobile))
                        if is_no_valid:
                            mobile = encrypt(str(mobile))
                        else:
                            logging.info("entered invalid mobile no")
                            return False, "CF16400", "Office contact number is not correct"
                else:
                    msg = "error"
                    if net_monthyl_income < 0 or len(str(net_monthyl_income)) >= 9:
                        msg = "Please Enter Valid Income"
                    return False, "CF16400", msg

            name, consumer_mobile, consumer_email, merchant_id = self._repository.consumer_name(application_id)
            consumer_mobile = decrypt(str(consumer_mobile))
            consumer_email = decrypt(str(consumer_email))
            if employement_type.lower() == "salaried" and merchant_id != 73:
                epf = epf_call_cf(company_name, name, consumer_mobile, consumer_email, application_id)
            company_id = self._repository.check_company_present(company_name)
            if not company_id and employement_type != 'unemployed' and company_name:
                company_id = self._repository.insert_company(company_name)
            elif employement_type == 'unemployed':
                company_id = None
            self._repository.update_employment_details(application_id, mobile, employement_type, net_monthyl_income,
                                                       company_id, gst_number)
            if not self._repository.fetch_status_log(application_id, 'in-process'):
                self._repository.update_application_state(consumer_mobile, "in-process", application_id)
            return True, "CF10200", "saved"
        except Exception as ex:
            logging.exception("An error occurred")

    def no_touch_process_product_details(self, app_id, project, project_sub_category, loan_amount, kw_installation, inverter_to_be_installed,
                                         panel_to_be_installed, subsidy, solar_power_type):
        try:
            solar_type_dict = {'On-grid': 1, 'Off-grid': 2, 'Hybrid': 3}
            solar_type_value = solar_type_dict.get(solar_power_type)
            if project:
                status = self._repository.save_product_details(app_id, project, loan_amount, kw_installation,
                                                               inverter_to_be_installed, panel_to_be_installed, subsidy, project_sub_category, solar_type_value)
            else:
                status = True
            if status:
                auto_decision_response = creditfair.run_auto_decision(app_id)
                if auto_decision_response["message"] == "Approved":
                    get_offers = creditfair.get_offers(app_id)
                    product_name = self._repository.get_product_name(app_id)
                    for item in get_offers.get('data').get('emi_summary'):
                        item['product'] = product_name
                    if get_offers["status-code"] == 1:
                        auto_decision_response['data'] = get_offers["data"]['emi_summary']
                    else:
                        auto_decision_response['data'] = get_offers["status-message"]
                return auto_decision_response, 'SF10200'
            else:
                return None, 'SF10409'
        except Exception as ex:
            logging.exception("An error occurred")

    def get_no_touch_user_journey_details(self, mobile, app_id=None):
        try:
            if app_id is None:
                app_id = self._repository.get_application_id(mobile)
            if app_id:
                decision_path = None
                application_details = self._repository.get_application_details(app_id)
                application_element_details = self._repository.get_consumer_element_details(app_id)
                credit_report_status = self._repository.fetch_credit_report(app_id)
                category_id = self._repository.get_merchant_category(application_details.get('merchant_id'))
                name = application_details.get('name') if application_details.get('name') else ''
                name_split = name.split(' ') if name else ''
                first_name = name_split[0] if len(name_split) > 0 else ''
                last_name = name_split[-1] if len(name_split) > 1 else ''
                gender = application_details.get('gender', None)
                loan_amount = application_details.get('approved_amount', 0)
                email = decrypt(application_details.get('email')) if application_details.get(
                    'email') else ''  # update application page
                pan_number = decrypt(application_details.get('pan_card'))  # create application page
                dob = application_details.get('dob').strftime('%Y-%m-%d')
                addr_1 = application_details.get('permanent_address_line_1') if application_details.get(
                    'permanent_address_line_1') else ''
                installation_pincode = application_element_details.get('installation_address_pincode')
                solar_upload_docs = self._repository.check_solar_files(app_id, )
                bank_statement = self._repository.check_document(app_id, 'bank_statement')
                account_aggr_file = self._repository.get_document_by_file_type(app_id, '%Account Aggregator%')
                if addr_1:
                    addr_list = addr_1.split(' ')
                    addr_1 = " ".join(addr_list[:4])
                    addr_2 = " ".join(addr_list[4:8])
                    addr_3 = " ".join(addr_list[8:])
                    address_status = application_details.get('permanent_address_status')
                else:
                    addr_1, addr_2, addr_3, address_status = '', '', '', ''
                pincode = application_details.get('permanent_address_pincode') if application_details.get(
                    'permanent_address_pincode') else None
                city = application_details.get('permanent_address_city') if application_details.get(
                    'permanent_address_city') else None
                state = application_details.get('permanent_address_state') if application_details.get(
                    'permanent_address_state') else None
                create_application = True if pan_number else False
                update_application = True if email else False
                installation_addr_status = True if installation_pincode else False
                utility_bill_status = True if application_element_details.get(
                    'inverter_monthly_power_bill') is not None else False  # electricity bill
                solar_details = application_element_details.get('type_of_customer')
                upload_doc_status = True if solar_upload_docs or solar_details else False
                employment_status = True if application_details.get('employee_status') else False  # employment details
                account_aggregator = True if bank_statement or account_aggr_file else False
                approval_log_data = self._repository.get_approval_status_log(app_id)
                bre_status = True if approval_log_data else False
                bureau_page = True if not credit_report_status else False
                payment_page = True if self._repository.get_payment_log(app_id) else False
                payment_data = self._repository.get_payment_details(app_id, application_details.get("offer_id")) if not payment_page else None

                plan_offer_saved = True
                if bre_status:
                    if application_details.get('state') == 'cf approved':
                        if not application_details.get('offer_id'):
                            plan_offer_saved = False
                if category_id == config.SOLAR_CATEGORY_ID:
                    status_dict = {
                        'create_application': {'status': create_application, 'section': 0, 'page': 2},
                        'update_application': {'status': update_application, 'section': 1, 'page': 0},
                        'installation_addr': {'status': installation_addr_status, 'section': 1, 'page': 0},
                        'employment_details': {'status': employment_status, 'section': 2, 'page': 0},
                        'electricity_bill_validation': {'status': utility_bill_status, 'section': 3, 'page': 0},
                        'upload_documents': {'status': upload_doc_status, 'section': 3, 'page': 1},
                        'product_details': {'status': solar_details, 'section': 3, 'page': 2},
                        'plan_selected': {'status': plan_offer_saved, 'section': 4, 'page': 0}
                    }
                else:
                    status_dict = {
                        'create_application': {'status': create_application, 'section': 0, 'page': 2},
                        'update_application': {'status': update_application, 'section': 1, 'page': 0},
                        'employment_details': {'status': employment_status, 'section': 2, 'page': 0},
                        'plan_selected': {'status': plan_offer_saved, 'section': 3, 'page': 0},
                        # 'playment_details': {'status': payment_page, 'section': 3, 'page': 2}
                    }
                if application_details.get('state') in ['waiting for decision', 'rejected']:
                    del status_dict['plan_selected']
                if application_details.get("state") in ["disbursed", "nach registered"]:
                    show_decision, section, page = True, None, None
                else:
                    show_decision, section, page = self.get_section_page(status_dict)
                if show_decision:
                    decision_path = self.get_no_touch_decision_value(application_details.get('state'),
                                                                     account_aggregator,
                                                                     bureau_page)
                response_data = {
                    'application_id': app_id,
                    'merchant_id': application_details.get('merchant_id'),
                    'center_id': application_details.get('merchant_location_id'),
                    'category_id': category_id,
                    'section': section,
                    'page': page,
                    'basic_details': {
                        'first_name': first_name,
                        'last_name': last_name,
                        'mobile_number': mobile,
                        'gender': gender,
                        'email': email,
                        'addr1': addr_1,
                        'addr2': addr_2,
                        'addr3': addr_3,
                        'address_status': address_status.capitalize() if address_status else None,
                        'pincode': pincode,
                        'city': city,
                        'state': state,
                        'dob': dob,
                        'pan_number': pan_number,
                        'loan_amount': loan_amount,
                        'SELECTED_OFFER': payment_data
                    },
                    'decision': {
                        'status': show_decision,
                        'value': decision_path
                    }
                }
                return True, response_data, 'SF10200'
            else:
                response_data = {
                    'section': 0,
                    'page': 2,
                    'basic_details': {
                        'mobile_number': mobile
                    },
                    'decision': {
                        'status': False,
                        'value': None
                    }
                }
                return False, response_data, 'SF10200'
        except Exception as ex:
            logging.exception("An error occurred")

    def no_touch_get_bureau(self, app_id, first_name, last_name, pan_card, mobile_number, address_line_1,
                            address_line_2,
                            address_line_3, pincode, city, state, dob, url, queue, loan_amt):
        try:
            full_address = address_line_1 + ' ' + address_line_2
            data = {
                "application_id": str(app_id),
                "applicant_type": 1,
                "request_type": "customer",
                "first_name": first_name,
                "last_name": last_name,
                "pan_card": pan_card,
                "document_type": "PAN_DETAILED",
                "consent": "Y",
                "mobile_number": mobile_number,
                "gender": "1",
                "dob": dob,
                "address": full_address,
                "state": state,
                "city": city,
                "postal_code": pincode,
                "loan_amount": loan_amt,
                "vendor": "no_touch",
                "url": url
            }
            if queue:
                payload = {
                    "cmd": "command:NoTouchBureauCall",
                    "con": "jobs",
                    "param": data,
                    "job_handler": "App\\Helpers\\CmdHelper@runCommand"
                }
                if not self._repository.fetch_status_log(app_id, 'in-process'):
                    self._repository.insert_to_queue(json.dumps(payload), app_id, "NoTouchBureauCall")
            else:
                response_code, response = call_bureau_service(data)
                if response == 'Fetched Bureau Successfully':
                    return True, 'SF10200'
            return True, 'SF10200'
        except Exception as ex:
            logging.exception("An error occurred")

    def no_touch_ckyc(self, pan_number, dob, updated_by, application_id):
        try:
            pan_number = pan_number.upper()
            is_data_valid, code, data = no_touch_validate_ckyc(pan_number, dob)
            if is_data_valid:
                random_id = generate_unique_string()
                print("Random Unique String with Prefix:", random_id)
                response = creditfair.no_touch_callckyc(pan_number, dob, random_id, application_id)
                status = self._repository.update_ckyc_application_id(application_id, random_id)
                if response["status"] == "OK" and response["kycStatus"] == "SUCCESS":
                    response2 = response["data"]
                    response2["message"] = response["message"]
                else:
                    data = {
                        "status": "FAIL",
                        "message": "Something went wrong",
                        "data": {
                            "pan_number": pan_number,
                            "dob": dob
                        }
                    }
                    response2 = {"message": response["message"]}
                return 'CF10200', response2
            else:
                return code, data
        except Exception as ex:
            logging.exception("An error occurred")

    def generate_consent_document(self, application_id, latitude='', longitude='', file_name='consent_logs'):
        try:
            query_status, application_details = self._repository.get_consent_documents_details(application_id)
            data = {
                'lender_id': 2,
                'name': application_details.get("name"),
                'loan_application_no': f"{application_details.get('slug')}0{application_id}",
                'mobile': decrypt(application_details.get("mobile_number")),
                'ip': application_details.get("ips") if application_details.get("ips") is not None else None,
                'consent_timestamp': datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
                'date': datetime.now().date().strftime("%d-%b-%Y"),
                'logo_url': 'http://creditfair-customer-documents.s3.ap-south-1.amazonaws.com/rebranded-logos/logo1.png',
                'latitude': latitude,
                'longitude': longitude
            }
            consent_log_html = f"""<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
                    <html>
                    <head>
                        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
                        <meta http-equiv="X-UA-Compatible" content="IE=edge">
                        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
                        <meta http-equiv="X-UA-Compatible" content="IE=8">
                        <style>
                            table.out {{
                                width: 100%;
                                margin-bottom: 30px;
                                font-size: 14px;
                                border-collapse: collapse;
                            }}
    
                            table.out-joint {{
                                width: 99.4%;
                                font-size: 23px;
                                border-collapse: collapse;
                                margin-bottom: -33px;
                                margin-left: 3px;
                                text-align: center;
                                font-weight: 800;
                            }}
    
                            table.out-joint > tbody > tr > td {{
                                padding: 2px 2px 2px 8px;
                                font-size: 20px;
                                border: 1px solid black;
                            }}
    
                            table.out-joint-loan {{
                                width: 100%;
                                font-size: 23px;
                                border-collapse: collapse;
                                margin-bottom: -31px;
                                text-align: center;
                                font-weight: 800;
                                margin-top: 25px;
                            }}
    
                            table.out-joint-loan > tbody > tr > td {{
                                padding: 2px 2px 2px 8px;
                                font-size: 20px;
                                border: 1px solid black;
                            }}
    
                            table.out > tbody > tr > td {{
                                padding: 10px 10px 10px 10px;
                                font-size: 15px;
                                border: 1px solid black;
                            }}
    
                            table.out > tbody > tr > th {{
                                padding: 2px 2px 2px 8px;
                                font-size: 15px;
                                border: 1px solid black;
                            }}
    
                            li {{
                                padding-top: 8px;
                            }}
                            .header{{
                                background-color: #0fc3cc;
                                padding: 5px;
                                color: white;
                                margin-top: 23px;
                            }}
                            .ft8{{
                                font-weight: 700;
                            }}
                            p.ft9{{
                                font-weight: 700;
                            }}
                            p.ft9 > span{{
                                font-weight: 100;
                            }}
                            p.ft10{{
                                font-weight: 700;
                            }}
                            p.ft10 > span{{
                                font-weight: 100;
                            }}
                        </style>
                    </head>
    
                    <body style="background-color: whitesmoke;">
                    <div id="page_1">
                        <div id="p1dimg1">
                            <img src="{data['logo_url']}" style="outline:none;text-decoration:none;width:150px;padding-top:16px;padding-bottom:16px;margin-left:auto;margin-right:auto">
                            <div style="text-align: center;font-weight: 800;padding-top: 10px;padding-bottom: 10px; ">Consent Logs {data['name']} {data['date']}</div>
    
                                <table class="out">
                                        <tbody>
                                        <tr>
                                            <td><b>Loan Application No.</b></td>
                                            <td>{data['loan_application_no']}</td>
                                        </tr>
                                        <tr>
                                            <td><b>Name</b></td>
                                            <td>{data['name']}</td>
                                        </tr>
                                        <tr>
                                            <td><b>Mobile</b></td>
                                            <td>{data['mobile']}</td>
                                        </tr>
                                        <tr>
                                            <td><b>IP</b></td>
                                            <td>{data['ip']}</td>
                                        </tr>
                                        <tr>
                                            <td><b>Latitude</b></td>
                                            <td>{data['latitude']}</td>
                                        </tr>
                                        <tr>
                                            <td><b>Longitude</b></td>
                                            <td>{data['longitude']}</td>
                                        </tr>
                                        <tr>
                                            <td><b>Consent Timestamp</b></td>
                                            <td>{data['consent_timestamp']}</td>
                                        </tr>
                                        </tbody>
                                </table>
                        </div>
                    </div>
                    </body>
                    </html>"""
            pdf_bytes = pdfkit.from_string(consent_log_html, False)
            base64_consent_document = base64.b64encode(pdf_bytes).decode("utf-8")
            file_path = str(application_id) + '/consent_logs.pdf'
            upload_status = s3_upload_update(base64_consent_document, file_path)
            if upload_status:
                self._repository.insert_document(application_id, None, file_name, file_path, None, None, 107)
            return True
        except Exception as ex:
            logging.exception(traceback.print_exc())
            return False

    def consent_job(self, application_id, latitude, longitude, file_name):
        return self._repository.create_consent_document_job(application_id, latitude, longitude, file_name,
                                                            self._repository)

    def update_consent(self, application_id, latitude, longitude, status, request):
        try:
            user_ip = get_client_ip(request)
            self._repository.save_consent(application_id, latitude=latitude, longitude=longitude, status=status, user_ip=user_ip)
            return True
        except Exception as ex:
            logging.exception(traceback.print_exc())
            return False

    def update_loan_offer(self, app_id, offer_id, origin):
        try:
            if self._repository.is_agreement_signed(app_id):
                logging.info(f"Agreement already signed for app_id={app_id}")
                return False, 'CF10409'

            application = self._repository.get_application_details(app_id)
            if not application:
                return False, 'CF10400'

            loan_decision = self._repository.get_payment_details(app_id, offer_id)
            if not loan_decision:
                logging.info(f"Loan decision not found for app_id={app_id}, offer_id={offer_id}")
                return False, 'CF10400'

            merchant_offer = self._repository.get_merchant_offer(offer_id)
            if not merchant_offer:
                logging.info(f"Merchant offer not found for offer_id={offer_id}")
                return False, 'CF10400'

            merchant = self._repository.get_merchant_for_application(app_id)
            if not merchant:
                logging.info(f"Merchant not found for app_id={app_id}")
                return False, 'CF10400'

            approved_amount = application.get('approved_amount', 0) or 0
            upfront_from_customer = loan_decision.get('up_front_from_customer', 0) or 0
            charge_subvention_remaining_amt = merchant.get('charge_subvention_remaining_amount', 0) or 0

            upfront_from_cf = self._calculate_upfront_from_cf(
                approved_amount, loan_decision, merchant_offer, upfront_from_customer,
                charge_subvention_remaining_amt)

            received_by_merchant = (upfront_from_customer or 0) + (upfront_from_cf or 0)
            payable_by_customer = loan_decision.get('total', 0) or 0

            subvention_cgst, subvention_sgst, subvention_igst = self._calculate_gst_on_subvention(
                upfront_from_customer, approved_amount, merchant_offer, merchant)

            net_loan_amount = self._calculate_net_loan_amount(merchant_offer, approved_amount)
            emi_date = self._calculate_emi_date()

            existing_emi = self._repository.get_emi_calculation_details(app_id)
            now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")

            if existing_emi:
                re_scheduled_by = application.get('user_id')
                self._repository.insert_emi_calculation_lock(existing_emi, re_scheduled_by, approved_amount, now)

            self._repository.upsert_emi_calculation(
                app_id=app_id,
                offer_id=offer_id,
                tenure=merchant_offer.get('tenure'),
                emi=loan_decision.get('emi'),
                interest_rate=loan_decision.get('interest_rate'),
                remaining_emi=loan_decision.get('tenure'),
                advance_emi=merchant_offer.get('advance_emi'),
                subvention=merchant_offer.get('subvention'),
                processing_fee=loan_decision.get('processing_fee'),
                upfront_from_customer=upfront_from_customer,
                emi_date=emi_date,
                received_by_merchant=received_by_merchant,
                upfront_from_credit_fair=upfront_from_cf,
                payable_by_customer=payable_by_customer,
                annualised_irr=merchant_offer.get('annualised_irr'),
                payment_frequency=merchant_offer.get('payment_frequency'),
                disbursement_method=merchant_offer.get('disbursement_method'),
                subvention_cgst=subvention_cgst,
                subvention_sgst=subvention_sgst,
                subvention_igst=subvention_igst,
                net_loan_amount=net_loan_amount,
                now=now
            )

            self._repository.update_application_offer_tenure(app_id, offer_id, loan_decision.get('tenure'))

            job_type = ('emiCheckoutGenerateDocuments'
                        if origin in ['https://checkoutuat.creditfair.in', 'https://checkout.creditfair.in']
                        else 'noTouchDocuments')
            payload = {
                "cmd": f"command:{job_type}",
                "con": "jobs",
                "param": {
                    "id": str(app_id),
                    "status": "approved",
                    "previous_status": "cf approved",
                    "auth_user_id": 107,
                    "lender_id": application.get('lender_id'),
                    "documents": "[pro_forma_invoice, sanction_letter, cam]"
                },
                "job_handler": "App\\Helpers\\CmdHelper@runCommand"
            }
            self._repository.insert_to_queue(json.dumps(payload), app_id, job_type)

            return True, 'CF10200'
        except Exception as ex:
            logging.exception("An error occurred in update_loan_offer")
            return False, 'CF10400'

    @staticmethod
    def _calculate_upfront_from_cf(approved_amount, loan_decision, merchant_offer,
                                    upfront_from_customer, charge_subvention_remaining_amt):
        down_payment = upfront_from_customer if charge_subvention_remaining_amt == 1 else 0
        interest_type_id = merchant_offer.get('interest_type_id')
        subvention = merchant_offer.get('subvention', 0) or 0
        offer_tenure = merchant_offer.get('tenure') or 1
        decision_tenure = loan_decision.get('tenure') or 1
        interest_rate = merchant_offer.get('interest_rate')
        advance_type = merchant_offer.get('advance_type')

        if interest_type_id == 1:
            sub_amount = ((approved_amount - down_payment) * subvention) / 100
            if interest_rate:
                return approved_amount - upfront_from_customer - sub_amount
            else:
                upfront_from_cf = (approved_amount * (decision_tenure / offer_tenure)) - (
                    ((approved_amount - upfront_from_customer) * subvention) / 100)
                if advance_type == 'percentage' and upfront_from_customer and upfront_from_cf:
                    return upfront_from_cf - upfront_from_customer
                return upfront_from_cf

        elif interest_type_id == 2:
            return approved_amount - upfront_from_customer - (subvention * (approved_amount - down_payment)) / 100

        elif interest_type_id == 3:
            if interest_rate:
                return approved_amount - upfront_from_customer - (
                    (approved_amount - down_payment) * subvention) / 100
            else:
                return (approved_amount * (decision_tenure / offer_tenure) -
                        ((approved_amount - down_payment) * subvention) / 100)

        elif interest_type_id == 4:
            return approved_amount - upfront_from_customer - (
                ((approved_amount - down_payment) * subvention) / 100)

        return 0

    @staticmethod
    def _calculate_gst_on_subvention(upfront_from_customer, approved_amount, merchant_offer, merchant):
        subvention_cgst, subvention_sgst, subvention_igst = 0, 0, 0
        charge_subvention_remaining = merchant.get('charge_subvention_remaining_amount', 0) or 0
        subvention = merchant_offer.get('subvention', 0) or 0

        if charge_subvention_remaining == 1:
            subvention_amount = ((approved_amount - upfront_from_customer) * subvention) / 100
        else:
            subvention_amount = (approved_amount * subvention) / 100

        if subvention_amount > 0:
            is_gst = merchant.get('is_gst', 0) or 0
            state = merchant.get('state', '') or ''
            if is_gst == 1 and state:
                if state == 'Maharashtra':
                    subvention_cgst = round((subvention_amount * 9) / 100)
                    subvention_sgst = round((subvention_amount * 9) / 100)
                else:
                    subvention_igst = round((subvention_amount * 18) / 100)

        return subvention_cgst, subvention_sgst, subvention_igst

    @staticmethod
    def _calculate_net_loan_amount(merchant_offer, approved_amount):
        advance_type = merchant_offer.get('advance_type')
        advance_emi = merchant_offer.get('advance_emi', 0) or 0
        tenure = merchant_offer.get('tenure') or 1

        if not approved_amount or approved_amount <= 0:
            return 0

        if advance_type == 'emi':
            return (approved_amount * (tenure - advance_emi)) / tenure
        elif advance_type == 'fixed_amount':
            return approved_amount - advance_emi
        elif advance_type == 'percentage':
            return (approved_amount * (100 - advance_emi)) / 100

        return 0

    @staticmethod
    def _calculate_emi_date():
        from datetime import date as date_type
        today = date_type.today()
        if today.day > 20:
            if today.month + 1 > 12:
                return date_type(today.year + 1, 2, 5)
            elif today.month + 1 > 11:
                return date_type(today.year + 1, 1, 5)
            else:
                return date_type(today.year, today.month + 2, 5)
        else:
            if today.month + 1 > 12:
                return date_type(today.year + 1, 1, 5)
            return date_type(today.year, today.month + 1, 5)
