# 2023.06.21 - MA 
# QANAF_V2.PY
# V 8.EB1/V 4.EB1 
# V 8.EA3/V 4.EA3
# V 8.EA2/V 4.EA2
 
# Link la api ANAF
ANAF_API_URL    = "https://webservicesp.anaf.ro/PlatitorTvaRest/api/v8/ws/tva"
# Documentatie:
# https://static.anaf.ro/static/10/Anaf/Informatii_R/Servicii_web/doc_WS_V8.txt

#RESPONSE_FORMAT= "A"|"B"|"C"
RESPONSE_FORMAT = "{xx_response_format}"

# Debug 0 fara debug, 1 cu debug scrie si JSON
DEBUG           = {xx_debug}

# Numar cif-uri cerere.
MAX_CHUNK_SIZE  = 99

# Asteptare la raspuns serviciu ANAF. 10 secunde.
TIMEOUT         = 10

# Asteptare intre doua query.
WAIT_TIME       = 2

# Cale fisier output.
XF_CSV          = "{xf_csv_out}"

# Cale fisier arhiva
# daca este "" nu se executa nimic
# daca este "fisier.CSV" se va crea fisierul CSV cf. spec. RESPONSE FORMAT="C"
XF_ARH          = "{xf_arh_out}"

# Query pentru request.
{xx_query}               

##############################################################################

# Functii de program.

import requests
import json
import csv
import sys
import unicodedata
import os
import pathlib
import time
import re

def chunk_list(array, chunk_size):
    for i in range(0, len(array), chunk_size):
        yield array[i:i + chunk_size]
    return array
    
def remove_accents(input_str):
    nfkd_form = unicodedata.normalize('NFKD', input_str)
    only_ascii = nfkd_form.encode('ASCII', 'ignore')
    return only_ascii.decode("cp1252") 

def remove_big_integers_from_query(query):
    max_int = 2147483647
    return [ row for row in query if int(row[0]) <= max_int]

def send_query(query):
    
    # Eliminare toate cif-uri mai mari decat max_int.
    # De exemplu: 4030102350 
    query = remove_big_integers_from_query(query)
        
    resp  = []
    
    def build_request(query):
        req  = [ {'cui':x[0], 'data' : x[1]} for x in query ]
        resp = requests.post(url     = ANAF_API_URL,
                             headers = {'Content-Type' : 'application/json'},
                             json    = req,
                             timeout = TIMEOUT)
        resp_str = resp.content.decode('utf-8')
        resp_str = remove_accents(resp_str)
        
        if resp.status_code != 200:
            raise requests.RequestException("ANAF nu poate fi accesat!")
        
        if len(resp_str) == 0:
            raise requests.RequestException("ANAF nu poate fi accesat!")
        
        try:
            resp     = json.loads(resp_str)
                
        except Exception as e:
            # Daca structura raspuns nu este structura JSON.
            raise Exception("Raspuns ANAF incorect!")       
            
        # Returneaza mesaj de eroare primit din serviciu ANAF.
        if resp.get('cod') == 501:
            raise Exception(resp.get('message'))     
            
        return resp
    
    if len(query) == 1:
        cif  = query[0][0]
        data = query[0][1]
        req  = [{'cui' : cif, 'data' : data}]
        resp = build_request(query)
    else:
        req = query
        if len(query) > MAX_CHUNK_SIZE:
            req  = chunk_list(req, MAX_CHUNK_SIZE)
            resp = {'found':[], 'notFound' : []}
            i    = 0
            req  = list(req)
            for x in req:
                print("Cerere date pe "+str(len(x))+" firme din "+str(len(QUERY))+" firme ...")
                anf_req        = build_request(x)
                #print("Pregatire date preluate ...")
                found_list     = anf_req.get('found')
                resp_f_list    = resp.get('found')
                [ resp_f_list.append(x) for x in found_list ]
                
                not_found_list = anf_req.get('notFound')
                resp_nf_list   = resp.get('notFound')
                [ resp_nf_list.append(x) for x in not_found_list ]
                  
                # Asteptare WAIT_TIME secunde inainte de trimitere request la ANAF.
                if i < len(req):
                    #print("Asteptare "+str(2)+" secunde")
                    time.sleep(WAIT_TIME)
                i+=1
        else:
            resp = build_request(req)
    
    # Scriere fisier JSON daca DEBUG=1
    # Indentare rezultat: json.dump(resp, json_file, indent=4)
    if DEBUG == 1:
        out_json_file = XF_CSV.replace(pathlib.Path(XF_CSV).suffix,'.JSON')
        with open(out_json_file,"w") as json_file:
            json.dump(resp, json_file)
            
    return resp
    
    
def walk_dict_layers(d, depth=0, callback=None, keys=[]):
    for k, v in d.items():
        if isinstance(v, dict):
            if callback:
                callback('#'+k, '', depth)
            walk_dict_layers(v, depth+2, callback, keys)
        elif isinstance(v, list):
            for row in v:
                if callback:
                    callback('#'+k, '', depth)
                walk_dict_layers(row, depth+2, callback, keys)
        else:
            keys.append(k) if k not in keys else None
            if callback:
                callback(k, v, depth)    
    return keys
    
def format_A(csv_writer,
             data_set,
             print_header = True):
             
    if print_header:
        csv_writer.writerow(['CIF/DATA','ATRIBUTE'])
        
    for row in data_set:
        if type(row) == dict:
            cif                 = str(row.get('date_generale').get('cui'))
            data                = str(row.get('date_generale').get('data'))
            isStatusRadiat      = False
            stare_inr           = row.get('date_generale').get('stare_inregistrare')
            rad_match           = re.search(r"\bRADIERE\b", stare_inr) 
            date_match          = re.search(r"\b\d{2}\.\d{2}\.\d{4}\b", stare_inr)  
            if rad_match and date_match:
                date_radiat1    = date_match.group()
                day,month,year  = date_radiat1.split(".")
                date_radiat2    = f"{year}-{month}-{day}"
                isStatusRadiat  = data>=date_radiat2 
            isStatusInactiv     = row.get('stare_inactiv').get('statusInactivi')         
            isStatus            = 'D' if isStatusRadiat or isStatusInactiv                      == True else 'N'
            isScpTVA            = 'D' if row.get('inregistrare_scop_Tva').get('scpTVA')         == True else 'N'
            isStatusTVAIncasare = 'D' if row.get('inregistrare_RTVAI').get('statusTvaIncasare') == True else 'N'
            isEfactura          = 'D' if row.get('date_generale').get('statusRO_e_Factura')     == True else 'N'
            csv_writer.writerow([cif+'/'+data, isStatus + isScpTVA + isStatusTVAIncasare + isEfactura])
        else:
            cif = str(row)
            csv_writer.writerow([cif+'/ERR',""])

    
def format_B(csv_writer,
             data_set,
             print_header = True):
    
    if print_header:
        csv_writer.writerow(['ATRIBUT','VALOARE'])

    for row in data_set:
        if type(row) == dict:
            cif  = str(row.get('date_generale').get('cui'))
            data = str(row.get('date_generale').get('data'))
            csv_writer.writerow(['CIF/DATA', cif+'/'+data])
            walk_dict_layers(row, callback=lambda k,v,depth: csv_writer.writerow([k, v]))
        else:
            cif = str(row)
            csv_writer.writerow(['CIF/DATA',cif+'/ERR'])
    
def format_C(csv_writer,
             data_set,
             print_header = True):
    csv_header = ['CIF/DATA'] 
    csv_rows   = []
    i          = 0
    for row in data_set:
        csv_row = []
        if type(row) == dict:
            cif     = str(row.get('date_generale').get('cui'))
            data    = str(row.get('date_generale').get('data'))
            header  = walk_dict_layers(row, callback=lambda k,v,depth: csv_row.append(v) if not k.startswith('#') else None)
            if i == 0:
                csv_header = csv_header + header
            csv_rows.append([cif+'/'+data]+csv_row)
            i+=1
        else:
            cif = str(row)
            csv_rows.append([cif+'/ERR']) 

    if print_header:
        csv_writer.writerow(csv_header)
    csv_writer.writerows(csv_rows)
    
    
def format_response(data_set, 
                    response_format,
                    output_file):
    
    cifuri_found     = data_set.get('found')
    cifuri_not_found = data_set.get('notFound')
    
    # Verifica daca found si notFound contin valori.
    # In cazul in care nu contin valori, atunci eroare la partea ANAF.
    if len(cifuri_found) == 0 and len(cifuri_not_found) == 0:
        raise Exception("Eroare ANAF! Valori negasite in sectiune found si notFound!")
    
    with open(output_file, 'w', newline='') as csvfile:
        csv_writer = csv.writer(csvfile,
                                delimiter=';',
                                quoting=csv.QUOTE_MINIMAL)
        
        format_func = None
        
        if pathlib.Path(output_file).suffix.upper() == ".CSV":
            csvfile.write("SEP=;"+os.linesep)
        
        # Determinare format raspuns.
        if response_format   == 'A':
            format_func = format_A
        elif response_format == 'B':
            format_func = format_B
        elif response_format == 'C':
            format_func = format_C
        else:
            raise Exception("Format apel eronat!")
            
        # Scriere cif-uri gasite in fisier CSV.
        format_func(csv_writer, cifuri_found)
        
        # Scriere cif-uri negasite la sfarsit fisier CSV.
        if len(cifuri_not_found) > 0:
            format_func(csv_writer, cifuri_not_found, False)

def print_and_return_exit_code(err_msg):
    print(err_msg[2:])
    return int(err_msg[:1])


#################################################################################################

# Inceput program.

if __name__ == "__main__":
    
    # Error code. Daca egal cu 0 -> Program rulat corect.
    # Coduri de eroare:
    #        1 - Eroare la conexiune internet sau interogare cu serviciu ANAF.
    #        2 - Eroare la scriere fisier rezultat.
    #        3 - Alte eroare la formatare raspuns ANAF. 
    #            De exemplu:
    #                      - Format apel eronat
    #                      - Raspuns ANAF incorect.
    #                        Daca format data incorect.
    #                        Daca numarul de caractere in CIF mai mare decat 10.
    #                        Daca atributul 'cod' in raspuns ANAF egal cu 501. Returneaza valoare
    #                        atribut 'message' care contine mesaj de eroare.
    exit_code = 0
    
    try:
        print("Trimitere request ...")
        resp = send_query(QUERY)
        print("Formatare raspuns si scriere intr-un fisier ...")
        format_response(resp,
                        RESPONSE_FORMAT,
                        XF_CSV)
                        
        # Daca variabila XF_ARH reprezint o calea fisier
        # scriere fisier CSV in format C
        if XF_ARH:
            # Daca format raspuns este tip C, se copieaza fisier XF_CSV la XF_ARH
            # Altfel se genereaza un nou fisier de tip C
            if RESPONSE_FORMAT == "C":
                import shutil
                print("Copiere fisier:\n'"+XF_CSV+"'\n la\n'"+XF_ARH+"'")
                shutil.copy(XF_CSV, XF_ARH)
            else:
                print("Generare fisier: '"+XF_ARH+"'")
                format_response(resp,
                                "C",
                                XF_ARH)
                                
    # Gestionare eroare internet sau conexiune la ANAF
    except requests.RequestException as e:
        exit_code = print_and_return_exit_code("1. *** ANAF nu poate fi accesat!")
    # Gestionare eroare scriere fisier
    except IOError as e:
        exit_code = print_and_return_exit_code("2. *** Eroare la scriere fisier: "+str(e))
    # Gestionare eroare la formatare raspuns ANAF sau alte eroare la 
    except Exception as e:
        exit_code = print_and_return_exit_code("3. *** "+str(e))
        
    print("-"*50)
    print("Terminat!")
    
    sys.exit(exit_code)