"""
    Modo simulacao: a rotina executa normalmente, mas nada e enviado para a api nem gravado no banco
    - Ativado pelo argumento "simulacao" depois dos 9 parametros do ws.py (pode ser junto com o debug)
        python3 ws.py mercos GET produtos greatpets GREATPETS 1 1 ./saida.json ./saida.cobol simulacao
    - HTTP : GET/HEAD/OPTIONS executam, POST/PUT/PATCH/DELETE nao saem do servidor, vao para o log e retornam 200 com o proprio payload
             excecao: POST de autenticacao que so gera token novo (ver eh_autenticacao_liberada) executa de verdade
    - Banco: SELECT/SHOW/SET executam, INSERT/UPDATE/DELETE/CALL... nao executam e vao para o log (ver gn_mysql.executesql)
    - Tudo que foi simulado fica no arquivo de saida (log) com o status SIM
"""
import re, json, atexit
from datetime     import datetime
from urllib.parse import parse_qs, urlsplit

METODOS_LEITURA = ('GET', 'HEAD', 'OPTIONS')
SQL_LEITURA     = ('SELECT', 'SHOW', 'SET', 'WITH', 'EXPLAIN', 'DESCRIBE', 'DESC', 'USE')

"""
    Autenticacao liberada na simulacao: so o que a integracao ja faz a cada execucao sem gravar nada,
    gerando um token novo de curta duracao (o token anterior continua valido)
    - grant_type=refresh_token e authorization_code NUNCA sao liberados: o token antigo/codigo e invalidado
      e o novo nao seria gravado no banco (ex: bling, mercadolivre)
"""
GRANTS_LIBERADOS = ('client_credentials', 'password')

# Logins fora do padrao OAuth2 (sem grant_type no corpo): (regex da url, regex do corpo ou None)
LOGINS_LIBERADOS = [
        (r'/auth/?$'                                        , r'consumer_key')   # tray
    ,   (r'ideris\.com\.br/(.*/)?login/?$'                  , None)              # ideris
    ,   (r'/security/iam/v1/client-identities/login$'       , None)              # serasa
    ,   (r'/oauth2/v2/grant-token$'                         , None)              # cobranca_unicred
    ,   (r'/Repom\.ValePedagio\.WebAPI/api/Token$'          , None)              # repom (vale pedagio)
]

# Corpo com credenciais nao vai para o log
CAMPOS_SENSIVEIS = r'grant_type|client_secret|refresh_token|password|senha|consumer_secret'

_log_path = None
_total    = {'http': 0, 'sql': 0, 'auth': 0}


def ativo() -> bool:
    return _log_path is not None


def ativar(log_path:str, saida_cobol:str = '') :
    """ Liga o modo simulacao, deve ser chamado antes de instanciar a classe da integracao """
    global _log_path
    if ativo() :
        return

    _log_path = log_path
    _patch_requests()
    atexit.register(_finalizar)

    _escrever(
        f"SIMULACAO ativa: nenhum POST/PUT/PATCH/DELETE sera enviado para a api e nenhum INSERT/UPDATE/DELETE sera gravado no banco. Arquivo cobol: {saida_cobol}",
        flag = "w+"
    )


def registrar(mensagem:str) :
    _escrever(mensagem)


def bloquear_sql(sql:str, params:tuple = ()) -> bool :
    """ Retorna True (e registra no log) quando o comando altera o banco e a simulacao esta ativa """
    if not ativo() or not _comando_escrita(sql) :
        return False

    _total['sql'] += 1
    comando = ' '.join(str(sql).split())
    registrar(f"SQL nao executado: {comando}" + (f" | params: {params}" if params else ''))
    return True


def _comando_escrita(sql:str) -> bool :
    texto   = re.sub(r'/\*.*?\*/|--[^\n]*', ' ', str(sql), flags=re.S).strip().lstrip('(').strip().upper()
    palavra = texto.split(None, 1)[0] if texto else ''

    # Tabelas temporarias existem so na conexao atual
    if re.match(r'(CREATE|DROP)\s+TEMPORARY\s+TABLE', texto) :
        return False

    if palavra == 'SET' :
        return bool(re.match(r'SET\s+(GLOBAL|PERSIST)', texto))

    if palavra == 'WITH' :
        return bool(re.search(r'\b(INSERT|UPDATE|DELETE|REPLACE)\b', texto))

    return palavra not in SQL_LEITURA


def eh_autenticacao_liberada(url:str, body:str) -> bool :
    """ True quando o POST apenas gera um token novo, sem consumir/invalidar nada """
    grants = _grant_types(url, body)

    if grants :
        return all(grant in GRANTS_LIBERADOS for grant in grants)

    return any(
        re.search(url_regex, urlsplit(url).path if url_regex.startswith('/') else url, re.I)
        and (body_regex is None or re.search(body_regex, body))
        for url_regex, body_regex in LOGINS_LIBERADOS
    )


def _grant_types(url:str, body:str) -> list :
    """ grant_type enviado no corpo (form ou json) ou na query string """
    grants = parse_qs(urlsplit(url).query).get('grant_type', [])

    try :
        dados = json.loads(body)
        if isinstance(dados, dict) and dados.get('grant_type') :
            grants.append(str(dados['grant_type']))
    except Exception :
        grants += parse_qs(body).get('grant_type', [])

    return [grant.strip().lower() for grant in grants]


class CursorSimulado() :
    """ Retorno do gn_mysql.executesql para os comandos que nao foram executados """
    lastrowid   = 0
    rowcount    = 0
    description = None
    _rows       = ()

    def fetchall(self) :
        return ()

    def fetchone(self) :
        return None

    def __iter__(self) :
        return iter(())


def _patch_requests() :
    """
        Intercepta na camada de transporte do requests, assim pega o gn_request
        e tambem as integracoes que usam requests direto
    """
    from requests.adapters   import HTTPAdapter
    from requests.models     import Response
    from requests.structures import CaseInsensitiveDict

    send_original = HTTPAdapter.send

    def send(self, request, **kwargs) :
        if request.method.upper() in METODOS_LEITURA :
            return send_original(self, request, **kwargs)

        body = request.body or ''
        if isinstance(body, bytes) :
            body = body.decode('utf8', errors='replace')

        if request.method.upper() == 'POST' and eh_autenticacao_liberada(request.url, body) :
            _total['auth'] += 1
            registrar(f"POST de autenticacao liberado: {urlsplit(request.url)._replace(query='').geturl()}")
            return send_original(self, request, **kwargs)

        _total['http'] += 1
        sensivel = re.search(CAMPOS_SENSIVEIS, body, re.I) or re.search(r'token|login|auth', urlsplit(request.url).path, re.I)
        log_body = '(corpo com credenciais omitido)' if sensivel else body
        registrar(f"{request.method.upper()} nao enviado: {request.url} {log_body}")

        try :
            payload = json.loads(body)
        except Exception :
            payload = {}

        response             = Response()
        response.status_code = 200
        response.reason      = 'OK (SIMULACAO)'
        response.headers     = CaseInsensitiveDict({'Content-Type': 'application/json'})
        response._content    = json.dumps(payload if isinstance(payload, (dict, list)) else {}).encode('utf8')
        response.encoding    = 'utf8'
        response.url         = request.url
        response.request     = request
        response.connection  = self
        return response

    HTTPAdapter.send = send


def _finalizar() :
    registrar(
        f"SIMULACAO finalizada: {_total['http']} envio(s) para a api simulado(s), {_total['sql']} comando(s) SQL nao executado(s), {_total['auth']} autenticacao(oes) liberada(s)"
    )


def _escrever(mensagem:str, flag:str = "a+") :
    conteudo = f"{datetime.strftime(datetime.now(),'%d/%m/%Y %H:%M:%S')};SIM;{mensagem}\n"
    try :
        with open(_log_path, flag, encoding='utf8') as log :
            log.write(conteudo)
    except Exception as error :
        print(f"SIMULACAO - falha ao gravar log: {error}")
    print(conteudo, end='')
