unit uDM;

interface

uses
  System.SysUtils, System.Classes, FireDAC.Stan.Intf, FireDAC.Stan.Option,
  FireDAC.Stan.Error, FireDAC.UI.Intf, FireDAC.Phys.Intf, FireDAC.Stan.Def,
  FireDAC.Stan.Pool, FireDAC.Stan.Async, FireDAC.Phys, FireDAC.Phys.MySQL,
  FireDAC.Phys.MySQLDef, FireDAC.VCLUI.Wait, FireDAC.Comp.UI, Data.DB,
  FireDAC.Comp.Client, uConfig, Winapi.Windows, FireDAC.Stan.Param;

type
  TDM = class(TDataModule)
    FDConnection: TFDConnection;
    FDPhysMySQLDriverLink: TFDPhysMySQLDriverLink;
    FDGUIxWaitCursor: TFDGUIxWaitCursor;
    procedure DataModuleCreate(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
    procedure Connect;
    procedure LoadCompanyConfig;
    procedure Log(const ATipo, AProceso, AMensaje: string; const ANomArchivo: string = '');
    procedure ActualizarEstructurasConfig(AForce: Boolean = False);
    function GenerarPromptIA(const AProposito, AEstructura, ARequisitos: string): string;
    function OptimizarPromptIA(const APromptActual, AEspecificaciones: string): string;
  end;

var
  DM: TDM;

implementation

uses
  System.JSON, System.Net.HttpClient, System.Net.URLClient, System.Net.Mime, 
  System.Net.HttpClientComponent, System.Generics.Collections, System.IOUtils;

{$R *.dfm}

{ TDM }

procedure TDM.ActualizarEstructurasConfig(AForce: Boolean = False);
var
  QConfig, QEst: TFDQuery;
  Tablas: TArray<string>;
  Tabla, Estructura: string;
begin
  QConfig := TFDQuery.Create(nil);
  QEst := TFDQuery.Create(nil);
  try
    QConfig.Connection := FDConnection;
    QEst.Connection := FDConnection;
    
    QConfig.SQL.Text := 'SELECT * FROM gestion_documental_config';
    QConfig.Open;
    
    while not QConfig.Eof do
    begin
      // 1. Obtener Estructura si falta o si se fuerza
      if (QConfig.FieldByName('tablas_implicadas').AsString <> '') and 
         ((QConfig.FieldByName('estructura_tablas').AsString = '') or AForce) then
      begin
        Estructura := '';
        Tablas := QConfig.FieldByName('tablas_implicadas').AsString.Split([',']);
        for Tabla in Tablas do
        begin
          if Tabla.Trim = '' then Continue;
          
          QEst.SQL.Text := 'SELECT COLUMN_NAME, DATA_TYPE, COLUMN_COMMENT ' +
                          'FROM information_schema.COLUMNS ' +
                          'WHERE TABLE_SCHEMA = :db AND TABLE_NAME = :tab';
          QEst.ParamByName('db').AsString := Config.DBName;
          QEst.ParamByName('tab').AsString := Tabla.Trim;
          QEst.Open;
          
          if not QEst.Eof then
          begin
            Estructura := Estructura + 'Tabla: ' + Tabla.Trim + sLineBreak;
            while not QEst.Eof do
            begin
              Estructura := Estructura + ' - ' + QEst.FieldByName('COLUMN_NAME').AsString + 
                           ' (' + QEst.FieldByName('DATA_TYPE').AsString + ')';
              if QEst.FieldByName('COLUMN_COMMENT').AsString <> '' then
                Estructura := Estructura + ' [' + QEst.FieldByName('COLUMN_COMMENT').AsString + ']';
              Estructura := Estructura + sLineBreak;
              QEst.Next;
            end;
            Estructura := Estructura + sLineBreak;
          end;
          QEst.Close;
        end;
        
        if Estructura <> '' then
        begin
          FDConnection.ExecSQL('UPDATE gestion_documental_config SET estructura_tablas = :est WHERE id = :id',
            [Estructura, QConfig.FieldByName('id').AsInteger]);
        end;
      end;

      // 2. Generar Prompt si falta o si se fuerza
      if (QConfig.FieldByName('prompt_ia').AsString = '') or AForce then
      begin
        // Recargar Estructura (por si acaba de ser actualizada arriba)
        Estructura := Estructura; // Usamos la variable local si se acaba de calcular
        if Estructura = '' then
           Estructura := QConfig.FieldByName('estructura_tablas').AsString;
           
        if Estructura <> '' then
        begin
          FDConnection.ExecSQL('UPDATE gestion_documental_config SET prompt_ia = :prom WHERE id = :id',
            [GenerarPromptIA(QConfig.FieldByName('proposito').AsString, 
                             Estructura, 
                             QConfig.FieldByName('requisitos_prompt').AsString), QConfig.FieldByName('id').AsInteger]);
        end;
      end;

      QConfig.Next;
    end;
  finally
    QEst.Free;
    QConfig.Free;
  end;
end;

function TDM.GenerarPromptIA(const AProposito, AEstructura, ARequisitos: string): string;
var
  Instructions: string;
begin
  Instructions := 'Eres un experto en extracción de datos estructurados. Tu tarea es generar un PROMPT TÉCNICO para Gemini. ' +
                  'El prompt resultante debe instruir a Gemini para procesar un documento basándose en estas especificaciones:' + sLineBreak +
                  '1. PROPÓSITO: ' + AProposito + sLineBreak +
                  '2. ESTRUCTURA DB: ' + AEstructura + sLineBreak;

  if ARequisitos <> '' then
    Instructions := Instructions + '3. REQUISITOS ADICIONALES: ' + ARequisitos + sLineBreak;

  Instructions := Instructions + sLineBreak +
                  'El prompt generado DEBE garantizar que Gemini devuelva UNICAMENTE un objeto JSON con esta estructura:' + sLineBreak +
                  '{"fecha": "YYYY-MM-DD", "nif": "...", "nombre": "...", "numero": "...", "detalles": [...], ...}' + sLineBreak +
                  'Donde las claves coincidan con los nombres de los campos de la base de datos suministrada.' + sLineBreak +
                  'Importante: El prompt debe ser generalista para cualquier tipo de documento de este tipo.';

  // Le pedimos a Gemini que genere el prompt de sistema para que nosotros luego lo usemos para procesar el archivo.
  Result := OptimizarPromptIA('', Instructions);
end;

function TDM.OptimizarPromptIA(const APromptActual, AEspecificaciones: string): string;
var
  HTTP: TNetHTTPClient;
  RequestBody, ResponseJSON, CandidateObj, SubContentObj, ContentObj, TextPart: TJSONObject;
  ContentsArr, PartsArr, CandidatesArr, SubPartsArr: TJSONArray;
  Response: IHTTPResponse;
  URL, AIResponseText: string;
  SS: TStringStream;
begin
  Result := APromptActual;
  if (Config.ApiKey = '') or (Config.IAModelo = '') then
    LoadCompanyConfig;

  if (Config.ApiKey = '') or (Config.IAModelo = '') then Exit;

  HTTP := TNetHTTPClient.Create(nil);
  RequestBody := TJSONObject.Create;
  try
    URL := 'https://generativelanguage.googleapis.com/v1beta/models/' + Config.IAModelo + ':generateContent?key=' + Config.ApiKey;
    
    ContentsArr := TJSONArray.Create;
    ContentObj := TJSONObject.Create;
    PartsArr := TJSONArray.Create;
    
    TextPart := TJSONObject.Create;
    if APromptActual = '' then
      TextPart.AddPair('text', AEspecificaciones)
    else
      TextPart.AddPair('text', 'Optimiza el siguiente prompt de IA basándote en estas especificaciones adicionales. ' +
                               'Devuelve ÚNICAMENTE el texto del nuevo prompt optimizado. ' +
                               'Prompt Actual: ' + APromptActual + sLineBreak +
                               'Especificaciones: ' + AEspecificaciones);
                               
    PartsArr.Add(TextPart);
    ContentObj.AddPair('parts', PartsArr);
    ContentsArr.Add(ContentObj);
    RequestBody.AddPair('contents', ContentsArr);

    HTTP.ContentType := 'application/json';
    SS := TStringStream.Create(RequestBody.ToJSON, TEncoding.UTF8);
    try
      Response := HTTP.Post(URL, SS);
    finally
      SS.Free;
    end;
    
    if Response.StatusCode = 200 then
    begin
      ResponseJSON := TJSONObject.ParseJSONValue(Response.ContentAsString) as TJSONObject;
      if Assigned(ResponseJSON) then
      try
        CandidatesArr := ResponseJSON.GetValue('candidates') as TJSONArray;
        if Assigned(CandidatesArr) and (CandidatesArr.Count > 0) then
        begin
          CandidateObj := CandidatesArr.Items[0] as TJSONObject;
          SubContentObj := CandidateObj.GetValue('content') as TJSONObject;
          SubPartsArr := SubContentObj.GetValue('parts') as TJSONArray;
          AIResponseText := (SubPartsArr.Items[0] as TJSONObject).GetValue('text').Value;
          Result := AIResponseText.Trim;
        end;
      finally
        ResponseJSON.Free;
      end;
    end;
  finally
    RequestBody.Free;
    HTTP.Free;
  end;
end;

procedure TDM.Log(const ATipo, AProceso, AMensaje: string; const ANomArchivo: string);
var
  Query: TFDQuery;
  LogFile, LogLine: string;
begin
  try
    LogFile := TPath.Combine(ExtractFilePath(ParamStr(0)), 'gestiondoc_process.log');
    LogLine := FormatDateTime('yyyy-mm-dd hh:nn:ss', Now) + ' [' + ATipo + '] ' + AProceso + ': ' + AMensaje;
    if ANomArchivo <> '' then
      LogLine := LogLine + ' (Archivo: ' + ANomArchivo + ')';
    TFile.AppendAllText(LogFile, LogLine + sLineBreak, TEncoding.UTF8);
  except
  end;

  Query := TFDQuery.Create(nil);
  try
    try
      Query.Connection := FDConnection;
      Query.SQL.Text := 'INSERT INTO ge_proceso_log (id_empresa, tipo, proceso, mensaje, nomarchivo) ' +
                       'VALUES (:ide, :tipo, :proc, :msg, :file)';
      
      Query.ParamByName('ide').DataType := ftInteger;
      Query.ParamByName('ide').AsInteger := StrToIntDef(Config.IdEmpresa, 1);
      
      Query.ParamByName('tipo').DataType := ftString;
      Query.ParamByName('tipo').Value := ATipo;
      
      Query.ParamByName('proc').DataType := ftString;
      Query.ParamByName('proc').Value := AProceso;
      
      Query.ParamByName('msg').DataType := ftString;
      Query.ParamByName('msg').Value := AMensaje;
      
      Query.ParamByName('file').DataType := ftString;
      if ANomArchivo <> '' then
        Query.ParamByName('file').Value := ANomArchivo
      else
        Query.ParamByName('file').Clear;
        
      Query.ExecSQL;
    except
    end;
  finally
    Query.Free;
  end;
end;

procedure TDM.Connect;
begin
  FDConnection.Close;
  FDConnection.Params.Values['Server'] := Config.DBServer;
  FDConnection.Params.Values['Database'] := Config.DBName;
  FDConnection.Params.Values['User_Name'] := Config.DBUser;
  FDConnection.Params.Values['Password'] := Config.DBPass;
  FDConnection.Params.Values['Port'] := Config.DBPort.ToString;
  FDConnection.Params.Values['CharacterSet'] := 'utf8mb4';
  FDConnection.Connected := True;
end;

procedure TDM.LoadCompanyConfig;
var
  Query: TFDQuery;
begin
  Query := TFDQuery.Create(nil);
  try
    Query.Connection := FDConnection;
    try
      Query.SQL.Text := 'SELECT IAmodelo, apikey FROM ge_empresas WHERE id = :id';
      Query.ParamByName('id').DataType := ftInteger;
      Query.ParamByName('id').AsInteger := StrToIntDef(Config.IdEmpresa, 1);
      Query.Open;
      if not Query.Eof then
      begin
        if Query.FieldByName('IAmodelo').AsString <> '' then
          Config.IAModelo := Query.FieldByName('IAmodelo').AsString;
        if Query.FieldByName('apikey').AsString <> '' then
          Config.ApiKey := Query.FieldByName('apikey').AsString;
      end;
      Query.Close;
    except
    end;
  finally
    Query.Free;
  end;
end;

procedure TDM.DataModuleCreate(Sender: TObject);
begin
  try
    Connect;
    LoadCompanyConfig;
    // Llamar a la actualización de configuración al inicio
    ActualizarEstructurasConfig;
  except
    on E: Exception do
      // En una tray app real, registraríamos esto en un log.
      OutputDebugString(PChar('Error de conexión DB: ' + E.Message));
  end;
end;

end.
