//=================================================================
// MCP Query for TOTVS Protheus - RXM Tecnologia (open source)
// Primeira implementacao MCP-compatible REST pro Protheus
// https://rxmtecnologia.com.br/blog/mcp-query-protheus-primeiro-mcp-server-open-source-consulta-tabelas
//
// Licenca: MIT
// Copyright (c) 2026 RXM Tecnologia
//
// Permissao concedida, gratuitamente, a qualquer pessoa que obtenha
// uma copia deste software para usar, copiar, modificar, mesclar,
// publicar, distribuir, sublicenciar e/ou vender copias do software.
//
// O SOFTWARE E FORNECIDO "COMO ESTA", SEM GARANTIA DE QUALQUER TIPO.
//=================================================================

#include "protheus.ch"
#include "restful.ch"

//-------------------------------------------------------------------
// MCP - Consulta generica de tabelas (v4)
// rawQuery + cursor pagination + SQL Injection protection + audit
//-------------------------------------------------------------------
WSRESTFUL McpQuery DESCRIPTION "MCP - Consulta generica v4"

	WSDATA tableName  AS STRING OPTIONAL
	WSDATA fields     AS STRING OPTIONAL
	WSDATA cWhere     AS STRING OPTIONAL
	WSDATA limit      AS STRING OPTIONAL
	WSDATA rawQuery   AS STRING OPTIONAL
	WSDATA offset     AS STRING OPTIONAL

	WSMETHOD GET DESCRIPTION "Consulta tabela" PATH "/query"

END WSRESTFUL

//-------------------------------------------------------------------
// GET /query
// Mode 1: ?tableName=SA1&fields=A1_COD,A1_NOME&cWhere=...&limit=50&offset=0
// Mode 2: ?rawQuery=SELECT ...&limit=500
//-------------------------------------------------------------------
WSMETHOD GET WSRECEIVE tableName, fields, cWhere, limit, rawQuery, offset WSSERVICE McpQuery

	Local cTabela   := Upper(AllTrim(Self:tableName))
	Local cFields   := AllTrim(Self:fields)
	Local cWhereCl  := AllTrim(Self:cWhere)
	Local cLimit    := Self:limit
	Local nLimit    := IIF(!Empty(cLimit), Val(cLimit), 100)
	Local cRawQuery := AllTrim(Self:rawQuery)
	Local nOffset   := IIF(!Empty(Self:offset), Val(Self:offset), 0)

	Local cQuery    := ""
	Local cSqlName  := ""
	Local aResult   := {}
	Local oJson     := JsonObject():New()
	Local cReject   := ""
	Local lRawMode  := !Empty(cRawQuery)

	::SetContentType("application/json")

	// === DEBUG ===
	If cTabela == "_DEBUG"
		oJson["FWCodEmp"]   := FWCodEmp()
		oJson["FWCodFil"]   := FWCodFil()
		oJson["cEmpAnt"]    := IIF(Type("cEmpAnt")=="C", cEmpAnt, "?")
		oJson["cFilAnt"]    := IIF(Type("cFilAnt")=="C", cFilAnt, "?")
		oJson["SIX_name"]   := McpFrameworkTable("SIX")
		oJson["SX7_name"]   := McpFrameworkTable("SX7")
		oJson["SX9_name"]   := McpFrameworkTable("SX9")
		oJson["SX1_name"]   := McpFrameworkTable("SX1")
		oJson["SA1_sql"]    := RetSqlName("SA1")
		oJson["SX2_sql"]    := RetSqlName("SX2")
		oJson["version"]    := "4.0.0"
		::SetResponse(oJson:ToJson())
		FreeObj(oJson)
		Return .T.
	EndIf

	// ================================================================
	// MODE 1: rawQuery (SELECT completo com SUM/GROUP BY)
	// ================================================================
	If lRawMode
		// Validar seguranca
		cReject := McpValidRawQuery(cRawQuery)
		If !Empty(cReject)
			McpAudit("INJECTION", "RAW", "rawQuery bloqueado: " + cReject)
			SetRestFault(400, '{"error":"rawQuery invalida: ' + cReject + '"}')
			Return .F.
		EndIf

		// Limite rawQuery: max 10000
		If nLimit > 10000
			nLimit := 10000
		EndIf
		If nLimit < 1
			nLimit := 1
		EndIf

		// Injetar TOP se ausente
		If !("TOP" $ Upper(cRawQuery))
			cRawQuery := "SELECT TOP " + cValToChar(nLimit) + " " + SubStr(cRawQuery, 8)
		EndIf

		McpAudit("RAW_QUERY", "SQL", cRawQuery)
		cQuery := cRawQuery

		// Executar e retornar
		Return McpExecuteQuery(Self, cQuery, nLimit, oJson, aResult)
	EndIf

	// ================================================================
	// MODE 2: tableName + fields + cWhere + offset (modo normal)
	// ================================================================

	// 1. Tabela obrigatoria
	If Empty(cTabela)
		SetRestFault(400, '{"error":"Parametro tableName ou rawQuery e obrigatorio"}')
		Return .F.
	EndIf

	// 2. Validar nome da tabela
	If !McpValidName(cTabela)
		SetRestFault(400, '{"error":"tableName invalido: somente letras, numeros e underscore"}')
		Return .F.
	EndIf

	// 3. Tabelas bloqueadas
	If McpIsBlocked(cTabela)
		McpAudit("BLOCKED", cTabela, "Tentativa de acesso a tabela bloqueada")
		SetRestFault(403, '{"error":"Acesso a tabela ' + cTabela + ' nao permitido"}')
		Return .F.
	EndIf

	// 4. Resolver nome SQL
	cSqlName := McpResolveSql(cTabela)
	If Empty(cSqlName)
		SetRestFault(404, '{"error":"Tabela ' + cTabela + ' nao encontrada"}')
		Return .F.
	EndIf

	// 5. Validar fields
	If !Empty(cFields)
		cReject := McpValidFields(cFields)
		If !Empty(cReject)
			McpAudit("INJECTION", cTabela, "Fields bloqueado: " + cReject)
			SetRestFault(400, '{"error":"Campo invalido detectado: ' + cReject + '"}')
			Return .F.
		EndIf
	Else
		cFields := "*"
	EndIf

	// 6. Validar cWhere
	If !Empty(cWhereCl)
		cReject := McpValidWhere(cWhereCl)
		If !Empty(cReject)
			McpAudit("INJECTION", cTabela, "Where bloqueado: " + cReject)
			SetRestFault(400, '{"error":"Clausula WHERE invalida: ' + cReject + '"}')
			Return .F.
		EndIf
	EndIf

	// 7. Limites
	If nLimit > 1000
		nLimit := 1000
	EndIf
	If nLimit < 1
		nLimit := 1
	EndIf

	// 8. Validar offset
	If nOffset < 0
		nOffset := 0
	EndIf

	// === MONTAR QUERY ===
	McpAudit("QUERY", cTabela, "fields=" + cFields + " where=" + cWhereCl + " limit=" + cValToChar(nLimit) + " offset=" + cValToChar(nOffset))

	cQuery := "SELECT TOP " + cValToChar(nLimit) + " "
	cQuery += cFields + " FROM " + cSqlName

	If McpIsFramework(cTabela)
		// Framework: sem D_E_L_E_T_
		If !Empty(cWhereCl) .Or. nOffset > 0
			cQuery += " WHERE "
			If !Empty(cWhereCl)
				cQuery += "(" + cWhereCl + ")"
			EndIf
			If nOffset > 0
				If !Empty(cWhereCl)
					cQuery += " AND "
				EndIf
				cQuery += "R_E_C_N_O_ > " + cValToChar(nOffset)
			EndIf
		EndIf
	Else
		// Negocio: com D_E_L_E_T_
		cQuery += " WHERE D_E_L_E_T_ = ' '"
		If !Empty(cWhereCl)
			cQuery += " AND (" + cWhereCl + ")"
		EndIf
		If nOffset > 0
			cQuery += " AND R_E_C_N_O_ > " + cValToChar(nOffset)
		EndIf
	EndIf

	// ORDER BY R_E_C_N_O_ pra paginacao deterministica
	cQuery += " ORDER BY R_E_C_N_O_"

	// Executar
Return McpExecuteQuery(Self, cQuery, nLimit, oJson, aResult)

// =================================================================
// McpExecuteQuery - Executa query e retorna JSON
// =================================================================
Static Function McpExecuteQuery(oSelf, cQuery, nLimit, oJson, aResult)
	Local cAlias   := GetNextAlias()
	Local oRow     := Nil
	Local n        := 0
	Local cFldName := ""

	ConOut("[McpQuery] " + cQuery)

	BEGIN SEQUENCE
		DbUseArea(.T., "TOPCONN", TcGenQry(,,cQuery), cAlias, .T., .T.)

		If (cAlias)->(Eof())
			(cAlias)->(DbCloseArea())
			oSelf:SetResponse('{"data":[],"count":0,"hasMore":false}')
			Return .T.
		EndIf

		While !(cAlias)->(Eof())
			oRow := JsonObject():New()
			For n := 1 To (cAlias)->(FCount())
				cFldName := AllTrim((cAlias)->(FieldName(n)))
				// Ocultar campos sensiveis (senhas, hashes, chaves)
				If McpIsSensitiveField(cFldName)
					oRow[cFldName] := "***REDACTED***"
				Else
					oRow[cFldName] := AllTrim(cValToChar((cAlias)->(FieldGet(n))))
				EndIf
			Next
			AAdd(aResult, oRow)
			(cAlias)->(DbSkip())
		EndDo

		(cAlias)->(DbCloseArea())

		RECOVER
		McpAudit("ERROR", "SQL", "Erro: " + cQuery)
		SetRestFault(500, '{"error":"Erro ao executar consulta"}')
		Return .F.
	END SEQUENCE

	// Resposta com hasMore
	oJson["data"]    := aResult
	oJson["count"]   := Len(aResult)
	oJson["hasMore"] := (Len(aResult) >= nLimit)

	oSelf:SetResponse(oJson:ToJson())
	FreeObj(oJson)

Return .T.

// =================================================================
// SEGURANCA: Validacao rawQuery (SELECT completo)
// Permite SUM/GROUP BY/ORDER BY mas bloqueia DDL/DML
// =================================================================
Static Function McpValidRawQuery(cQuery)
	Local cUpper   := Upper(AllTrim(cQuery))
	Local aBlocked := {}
	Local nI       := 0

	// Deve comecar com SELECT
	If Left(cUpper, 6) != "SELECT"
		Return "Query deve comecar com SELECT"
	EndIf

	// Bloquear DDL/DML, RCE, exfiltracao
	aBlocked := { ;
		"INSERT ",  "UPDATE ",  "DELETE ", ;
		"DROP ",    "ALTER ",   "CREATE ", ;
		"TRUNCATE", "GRANT ",   "REVOKE ", ;
		"EXEC ",    "EXEC(",    "EXECUTE ", ;
		"--",       "/*",       "*/", ;
		"XP_",      "SP_", ;
		"DECLARE",  "CURSOR",   "FETCH ", ;
		"WAITFOR",  "SHUTDOWN", ;
		"OPENROWSET", "OPENDATASOURCE", ;
		"DBCC",     "BULK ",    "CMDSHELL", ;
		"UNION ",   "UNION(", ;
		"CHAR(",    "NCHAR(",   "UNICODE(", ;
		"INTO ",    "OUTFILE",  "LOAD_FILE", ;
		"0X",       "0x" ;
		}

	For nI := 1 To Len(aBlocked)
		If aBlocked[nI] $ cUpper
			Return "Keyword bloqueada: " + AllTrim(aBlocked[nI])
		EndIf
	Next

	// Bloquear ponto-e-virgula (multi-statement)
	If ";" $ cQuery
		Return "ponto-e-virgula nao permitido"
	EndIf

Return ""

// =================================================================
// SEGURANCA: Validacao de campos (fields)
// =================================================================
Static Function McpValidFields(cFields)
	Local cUpper   := Upper(AllTrim(cFields))
	Local aBlocked := {}
	Local nI       := 0
	Local cChar    := ""

	aBlocked := {"SELECT", "INSERT", "UPDATE", "DELETE", "DROP", ;
		"ALTER", "CREATE", "TRUNCATE", "EXEC", "UNION", ;
		"--", "/*", "*/", ";", "XP_", "SP_", "CHAR(", ;
		"CAST(", "CONVERT(", "DECLARE", "CURSOR", "FETCH", ;
		"WAITFOR", "SHUTDOWN", "OPENROWSET", "OPENDATASOURCE"}

	For nI := 1 To Len(aBlocked)
		If aBlocked[nI] $ cUpper
			Return aBlocked[nI]
		EndIf
	Next

	For nI := 1 To Len(cFields)
		cChar := SubStr(cFields, nI, 1)
		If !McpIsSafeFieldChar(cChar)
			Return "caractere '" + cChar + "' nao permitido"
		EndIf
	Next

Return ""

Static Function McpIsSafeFieldChar(c)
	Local cUp := Upper(c)
	If cUp >= "A" .And. cUp <= "Z"
		Return .T.
	EndIf
	If cUp >= "0" .And. cUp <= "9"
		Return .T.
	EndIf
	If c == "_" .Or. c == "*" .Or. c == "," .Or. c == " " .Or. c == "."
		Return .T.
	EndIf
Return .F.

// =================================================================
// SEGURANCA: Validacao do WHERE
// =================================================================
Static Function McpValidWhere(cWhere)
	Local cUpper   := Upper(AllTrim(cWhere))
	Local aBlocked := {}
	Local nI       := 0

	aBlocked := {"INSERT", "UPDATE", "DELETE", "DROP", "ALTER", ;
		"CREATE", "TRUNCATE", "EXEC ", "EXECUTE", ;
		"UNION", "INTO ", "GRANT", "REVOKE", ;
		"--", "/*", "*/", "XP_", "SP_", ;
		"CHAR(", "CAST(", "CONVERT(", "DECLARE", ;
		"CURSOR", "FETCH", "WAITFOR", "SHUTDOWN", ;
		"OPENROWSET", "OPENDATASOURCE", "DBCC", ;
		"BULK", "CMDSHELL"}

	For nI := 1 To Len(aBlocked)
		If aBlocked[nI] $ cUpper
			Return aBlocked[nI]
		EndIf
	Next

	If ";" $ cWhere
		Return "ponto-e-virgula nao permitido"
	EndIf

	If "SELECT" $ cUpper
		Return "subselect nao permitido"
	EndIf

Return ""

// =================================================================
// SEGURANCA: Validacao de nome (tableName)
// =================================================================
Static Function McpValidName(cName)
	Local nI    := 0
	Local cChar := ""

	If Empty(cName) .Or. Len(cName) > 20
		Return .F.
	EndIf

	For nI := 1 To Len(cName)
		cChar := Upper(SubStr(cName, nI, 1))
		If !(cChar >= "A" .And. cChar <= "Z") .And. ;
				!(cChar >= "0" .And. cChar <= "9") .And. ;
				cChar != "_"
			Return .F.
		EndIf
	Next

Return .T.

// =================================================================
// SEGURANCA: Campos sensiveis (redacted na resposta)
// =================================================================
Static Function McpIsSensitiveField(cField)
	Local cUp := Upper(cField)
	// Senhas, hashes, tokens, chaves
	If "PSW"    $ cUp  // USR_PSWMD5, M0_PSW, M0_CTPSW, USR_PSWIGNPOL
		Return .T.
	EndIf
	If "CHKSUM" $ cUp  // USR_CHKSUM
		Return .T.
	EndIf
	If "LICENSA" $ cUp .Or. "CORPKEY" $ cUp  // M0_LICENSA, M0_CORPKEY
		Return .T.
	EndIf
	If "INTCTRL" $ cUp  // M0_INTCTRL
		Return .T.
	EndIf
	If "SECRET" $ cUp .Or. "TOKEN" $ cUp .Or. "APIKEY" $ cUp
		Return .T.
	EndIf
Return .F.

// =================================================================
// SEGURANCA: Tabelas bloqueadas
// =================================================================
Static Function McpIsBlocked(cTabela)
	Local aBlocked := {"SX8"}
	Local nI       := 0

	For nI := 1 To Len(aBlocked)
		If cTabela == aBlocked[nI]
			Return .T.
		EndIf
	Next

Return .F.

// =================================================================
// AUDITORIA
// =================================================================
Static Function McpAudit(cTipo, cTabela, cDetalhe)
	Local cLog := ""
	cLog := "[MCP-AUDIT] "
	cLog += "[" + cTipo + "] "
	cLog += DToC(Date()) + " " + Time() + " "
	cLog += "Tabela=" + cTabela + " "
	cLog += cDetalhe
	ConOut(cLog)
Return

// =================================================================
// RESOLUCAO DE NOMES SQL
// =================================================================
Static Function McpResolveSql(cTabela)
	Local cSqlName := ""

	If Left(cTabela, 4) == "SYS_"
		Return cTabela
	EndIf

	cSqlName := McpFrameworkTable(cTabela)
	If !Empty(cSqlName)
		Return cSqlName
	EndIf

	cSqlName := RetSqlName(cTabela)
	If !Empty(cSqlName)
		Return cSqlName
	EndIf

Return ""

// =================================================================
// TABELAS DE FRAMEWORK (sem D_E_L_E_T_)
// =================================================================
Static Function McpFrameworkTable(cTabela)
	Local cEmp := IIF(Type("cEmpAnt")=="C", cEmpAnt, FWCodEmp())
	Local aTabs := { ;
		"SX1", ;
		"SX7", ;
		"SX8", ;
		"SX9", ;
		"SXA", ;
		"SXB", ;
		"SXD", ;
		"SXE", ;
		"SXF", ;
		"SXG", ;
		"SIX"  ;
		}
	Local nI := 0

	For nI := 1 To Len(aTabs)
		If cTabela == aTabs[nI]
			Return cTabela + cEmp + "0"
		EndIf
	Next

Return ""

Static Function McpIsFramework(cTabela)
	If !Empty(McpFrameworkTable(cTabela))
		Return .T.
	EndIf
	If Left(cTabela, 4) == "SYS_"
		Return .T.
	EndIf
Return .F.
