unit uNcmAPI;

{ ============================================================================
   NCM API — Cliente Delphi
   ----------------------------------------------------------------------------
   Unit reutilizável para integrar qualquer ERP/aplicação Delphi com a API NCM.
   Requer Delphi XE8+ (2015) — usa System.Net.HttpClient (sem DLL externa).

   Como usar (resumo):

     uses uNcmAPI;

     var Api: TNcmAPI;
         R:   TNcmResultado;
     begin
       Api := TNcmAPI.Create('https://seu-dominio.com.br', 'ncm_live_xxx...');
       try
         R := Api.BuscarNCM('coca cola lata 350ml');
         if R.Sucesso then
           Memo1.Lines.Add(R.NCM + ' / CEST ' + R.CEST)
         else
           ShowMessage('Erro: ' + R.Erro);

         // Saldo atualizado a cada chamada:
         StatusBar1.Panels[0].Text := Format(
           'API NCM: %d / %d', [Api.QuotaUsed, Api.QuotaTotal]);
       finally
         Api.Free;
       end;
     end;

   Veja EXEMPLO_USO.md para cenários completos (cadastro, importação em
   lote em thread, tela de tributação, tratamento de cota esgotada, etc.).

   © Unique Sistemas — v1.0
   ========================================================================== }

interface

uses
  System.SysUtils, System.Classes, System.JSON, System.DateUtils,
  System.Net.HttpClient, System.Net.URLClient, System.NetEncoding,
  System.Generics.Collections;

type
  TConfianca = (cBaixa, cMedia, cAlta, cDesconhecida);
  TAmbiente  = (amProducao, amHomologacao);
  TRegime    = (rgSimples, rgPresumido, rgReal);
  TAtividade = (atDefault, atComercio, atIndustria);   // Default = comércio p/ Simples, indústria p/ Normal

  // ---- Resultado de classificação (buscar/batch) ----
  TNcmResultado = record
    Sucesso:       Boolean;     // false se houve erro
    Erro:          string;      // mensagem do erro (vazia se sucesso)
    Produto:       string;
    NCM:           string;      // 8 dígitos, sem pontos
    DescricaoNCM:  string;
    CEST:          string;      // 7 dígitos ou ''
    DescricaoCEST: string;
    Segmento:      string;
    TemST:         Boolean;
    NCMValidado:   Boolean;     // true se NCM existe na tabela oficial
    NCMVigente:    Boolean;     // true se ainda dentro da validade
    Confianca:     TConfianca;
    Justificativa: string;
    Fonte:         string;      // "cache" | "gemini" | "mock_homologacao"
  end;

  // ---- Detalhe de um NCM (GET /ncm/:codigo) ----
  TCestInfo = record
    Codigo:    string;
    Descricao: string;
    Segmento:  string;
  end;

  TNcmDetalhe = record
    Sucesso:     Boolean;
    Erro:        string;
    Codigo:      string;
    Descricao:   string;
    Vigente:     Boolean;
    ValidadeIni: TDateTime;
    ValidadeFim: TDateTime;
    TemST:       Boolean;
    Cests:       TArray<TCestInfo>;
  end;

  // ---- Saldo / Quota (GET /quota) ----
  TSaldo = record
    Sucesso:               Boolean;
    Erro:                  string;
    TenantNome:            string;
    TenantAtividade:       string;
    PlanoNome:             string;
    Ambiente:              string;
    VigenciaFim:           string;
    DiasParaVencer:        Integer;
    AssinaturaVencida:     Boolean;
    SaldoTotal:            Integer;
    SaldoUsado:            Integer;
    SaldoDisponivel:       Integer;
    PercentualUsado:       Double;
    PercentualDisponivel:  Double;
    CreditosExtras:        Integer;
    CustoNcmBuscar:        Integer;
    CustoNcmBatch:         Integer;
    CustoTributacao:       Integer;
    CustoCestGet:          Integer;
    HomologacaoGratis:     Boolean;
  end;

  // ---- Tributação completa (GET /ncm/:codigo/tributacao) ----
  TTributacao = record
    Sucesso:       Boolean;
    Erro:          string;
    NCM:           string;
    UF:            string;
    Regime:        string;
    TemST:         Boolean;
    CEST:          string;

    // Regime atual — ICMS
    AliqICMS:               Double;   // alíquota nominal/estatutária (referência IBPT — APENAS exibir)
    AliqICMSDestacadaNfe:   Double;   // o que vai no <vICMS> da NF-e (USE ESTE)
    CstICMS:                string;
    IcmsDestacarNfe:        Boolean;  // flag explícita
    IcmsObs:                string;   // explicação fiscal (CSOSN 500, CST 60, etc.)

    // Regime atual — PIS
    AliqPIS:                Double;
    AliqPISDestacadaNfe:    Double;   // 0 quando Simples — recolhe via DAS
    CstPIS:                 string;
    PisDestacarNfe:         Boolean;
    PisObs:                 string;

    // Regime atual — COFINS
    AliqCOFINS:             Double;
    AliqCOFINSDestacadaNfe: Double;
    CstCOFINS:              string;
    CofinsDestacarNfe:      Boolean;
    CofinsObs:              string;

    // Regime atual — IPI
    AliqIPI:                Double;
    CstIPI:                 string;
    IpiDestacarNfe:         Boolean;

    // IBPT (carga tributária aproximada)
    IbptFedNac:    Double;
    IbptFedImp:    Double;
    IbptEstadual:  Double;
    IbptMunicipal: Double;
    IbptCargaTotal:Double;
    IbptVersao:    string;

    // Reforma Tributária — IBS/CBS/IS
    AnoReforma:    Integer;
    AliqCBS:       Double;
    AliqIBS:       Double;
    TemIS:         Boolean;
    AliqIS:        Double;
    TotalIBSCBS:   Double;
    CstIbsCbs:     string;

    // Sugestões para emissão de NF-e
    CfopInterna:   string;
    CfopInteresta: string;

    // Análise dos CFOPs informados (se passados como parâmetro)
    CfopInternoAnalisado: record
      Informado:    Boolean;
      Conhecido:    Boolean;
      Descricao:    string;
      TipoOperacao: string;
      TemST:        Boolean;
      CstIcmsSug:   string;
      Alerta:       string;
    end;
    CfopExternoAnalisado: record
      Informado:    Boolean;
      Conhecido:    Boolean;
      Descricao:    string;
      TipoOperacao: string;
      TemST:        Boolean;
      CstIcmsSug:   string;
      Alerta:       string;
    end;

    // JSON completo para campos específicos não mapeados (uso avançado)
    Raw: TJSONObject;
  end;

  // ---- Exceções específicas ----
  ENcmAPIError      = class(Exception)
    StatusCode: Integer;
    constructor Create(const Msg: string; AStatus: Integer);
  end;
  ENcmAPIUnauthorized = class(ENcmAPIError);   // 401 — chave inválida/revogada
  ENcmAPIForbidden    = class(ENcmAPIError);   // 403 — tenant suspenso / key bloqueada
  ENcmAPIQuota        = class(ENcmAPIError);   // 402 — sem créditos
  ENcmAPINotFound     = class(ENcmAPIError);   // 404
  ENcmAPIBadRequest   = class(ENcmAPIError);   // 400

  // ---- Classe principal ----
  TNcmAPI = class
  private
    FHttp:     THTTPClient;
    FBaseURL:  string;
    FAPIKey:   string;
    FTimeout:  Integer;
    FQuotaTotal:     Integer;
    FQuotaUsed:      Integer;
    FQuotaRemaining: Integer;
    FQuotaReset:     string;
    FLastStatus:     Integer;
    FLastError:      string;

    function Request(const Method, Path: string;
                     const Body: TJSONValue = nil): TJSONValue;
    procedure ParseQuotaHeaders(Headers: TNetHeaders);
    procedure CheckHTTPError(AStatus: Integer; const ABody: string);
    function ParseConfianca(const S: string): TConfianca;
    function RegimeToStr(R: TRegime): string;
    function AtividadeToStr(A: TAtividade): string;
    function ParseResultado(J: TJSONObject): TNcmResultado;
  public
    constructor Create(const ABaseURL, AAPIKey: string;
                       ATimeoutSeconds: Integer = 60);
    destructor Destroy; override;

    // ----------- Endpoints principais -----------
    function BuscarNCM(const Produto: string): TNcmResultado;
    function BuscarBatch(const Produtos: TArray<string>): TArray<TNcmResultado>;
    function ObterNCM(const Codigo: string): TNcmDetalhe;
    function ObterTributacao(const Codigo, UF: string;
                             Regime: TRegime = rgPresumido;
                             const CfopInterno: string = '';
                             const CfopExterno: string = '';
                             Atividade: TAtividade = atDefault): TTributacao;
    function ObterCEST(const NCM: string): TArray<TCestInfo>;

    /// Consulta o saldo de créditos do tenant SEM cobrar créditos.
    /// Útil pra mostrar saldo na tela, validar antes de batch, etc.
    function ObterSaldo: TSaldo;

    // ----------- Utilitário -----------
    function TestarConexao: Boolean;

    // ----------- Propriedades -----------
    property BaseURL: string read FBaseURL write FBaseURL;
    property APIKey:  string read FAPIKey  write FAPIKey;
    property TimeoutSeconds: Integer read FTimeout write FTimeout;

    // Atualizados a cada chamada bem-sucedida (em produção):
    property QuotaTotal:     Integer read FQuotaTotal;
    property QuotaUsed:      Integer read FQuotaUsed;
    property QuotaRemaining: Integer read FQuotaRemaining;
    property QuotaReset:     string  read FQuotaReset;

    property LastStatus: Integer read FLastStatus;
    property LastError:  string  read FLastError;
  end;

implementation

{ ----------------------------------------------------------------------------
   ENcmAPIError
  ---------------------------------------------------------------------------- }

constructor ENcmAPIError.Create(const Msg: string; AStatus: Integer);
begin
  inherited Create(Msg);
  StatusCode := AStatus;
end;

{ ----------------------------------------------------------------------------
   TNcmAPI
  ---------------------------------------------------------------------------- }

constructor TNcmAPI.Create(const ABaseURL, AAPIKey: string;
  ATimeoutSeconds: Integer);
begin
  inherited Create;
  FBaseURL := ABaseURL;
  while FBaseURL.EndsWith('/') do
    SetLength(FBaseURL, Length(FBaseURL) - 1);
  FAPIKey  := AAPIKey;
  FTimeout := ATimeoutSeconds;

  FHttp := THTTPClient.Create;
  FHttp.ConnectionTimeout := FTimeout * 1000;
  FHttp.ResponseTimeout   := FTimeout * 1000;
  FHttp.UserAgent         := 'NcmAPI-Delphi/1.0';
  FHttp.AcceptCharSet     := 'utf-8';
end;

destructor TNcmAPI.Destroy;
begin
  FHttp.Free;
  inherited;
end;

procedure TNcmAPI.ParseQuotaHeaders(Headers: TNetHeaders);
var
  i: Integer;
  Name, Value: string;
begin
  for i := 0 to Length(Headers) - 1 do
  begin
    Name  := LowerCase(Headers[i].Name);
    Value := Headers[i].Value;
    if Name = 'x-ncm-quota-total' then
      FQuotaTotal := StrToIntDef(Value, 0)
    else if Name = 'x-ncm-quota-used' then
      FQuotaUsed := StrToIntDef(Value, 0)
    else if Name = 'x-ncm-quota-remaining' then
      FQuotaRemaining := StrToIntDef(Value, 0)
    else if Name = 'x-ncm-quota-reset' then
      FQuotaReset := Value;
  end;
end;

procedure TNcmAPI.CheckHTTPError(AStatus: Integer; const ABody: string);
var
  J: TJSONValue;
  Msg: string;
begin
  Msg := Format('HTTP %d', [AStatus]);
  J := nil;
  try
    J := TJSONObject.ParseJSONValue(ABody);
    if (J is TJSONObject) and (J.GetValue<string>('error', '') <> '') then
      Msg := J.GetValue<string>('error');
  finally
    J.Free;
  end;

  FLastStatus := AStatus;
  FLastError  := Msg;

  case AStatus of
    400: raise ENcmAPIBadRequest.Create(Msg, AStatus);
    401: raise ENcmAPIUnauthorized.Create(Msg, AStatus);
    402: raise ENcmAPIQuota.Create(Msg, AStatus);
    403: raise ENcmAPIForbidden.Create(Msg, AStatus);
    404: raise ENcmAPINotFound.Create(Msg, AStatus);
  else
    raise ENcmAPIError.Create(Msg, AStatus);
  end;
end;

function TNcmAPI.Request(const Method, Path: string;
  const Body: TJSONValue): TJSONValue;
var
  URL: string;
  Req: IHTTPRequest;
  Resp: IHTTPResponse;
  Source: TStringStream;
  RespText: string;
begin
  URL := FBaseURL + Path;
  Source := nil;

  Req := FHttp.GetRequest(Method, URL);
  Req.Headers.Add(TNetHeader.Create('X-API-Key', FAPIKey));
  Req.Headers.Add(TNetHeader.Create('Accept',    'application/json'));

  if Assigned(Body) then
  begin
    Source := TStringStream.Create(Body.ToJSON, TEncoding.UTF8);
    Req.SourceStream := Source;
    Req.Headers.Add(TNetHeader.Create('Content-Type', 'application/json; charset=utf-8'));
  end;

  try
    Resp := FHttp.Execute(Req);
    RespText := Resp.ContentAsString(TEncoding.UTF8);
    FLastStatus := Resp.StatusCode;
    ParseQuotaHeaders(Resp.Headers);

    if (Resp.StatusCode >= 200) and (Resp.StatusCode < 300) then
    begin
      FLastError := '';
      Result := TJSONObject.ParseJSONValue(RespText);
      if Result = nil then
        raise ENcmAPIError.Create('Resposta JSON inválida', Resp.StatusCode);
    end
    else
    begin
      Result := nil;
      CheckHTTPError(Resp.StatusCode, RespText);
    end;
  finally
    Source.Free;
  end;
end;

function TNcmAPI.ParseConfianca(const S: string): TConfianca;
begin
  if SameText(S, 'alta')  then Result := cAlta
  else if SameText(S, 'media') then Result := cMedia
  else if SameText(S, 'baixa') then Result := cBaixa
  else Result := cDesconhecida;
end;

function TNcmAPI.RegimeToStr(R: TRegime): string;
begin
  case R of
    rgSimples:   Result := 'simples';
    rgPresumido: Result := 'presumido';
    rgReal:      Result := 'real';
  else
    Result := 'presumido';
  end;
end;

function TNcmAPI.AtividadeToStr(A: TAtividade): string;
begin
  case A of
    atComercio:  Result := 'comercio';
    atIndustria: Result := 'industria';
  else
    Result := '';   // atDefault: nada — API decide pelo regime
  end;
end;

function TNcmAPI.ParseResultado(J: TJSONObject): TNcmResultado;
begin
  FillChar(Result, SizeOf(Result), 0);
  if J = nil then begin
    Result.Sucesso := False;
    Result.Erro    := 'Resposta vazia';
    Exit;
  end;
  if J.GetValue('erro') <> nil then begin
    Result.Sucesso := False;
    Result.Produto := J.GetValue<string>('produto', '');
    Result.Erro    := J.GetValue<string>('erro');
    Exit;
  end;
  Result.Sucesso       := True;
  Result.Produto       := J.GetValue<string> ('produto',       '');
  Result.NCM           := J.GetValue<string> ('ncm',           '');
  Result.DescricaoNCM  := J.GetValue<string> ('descricao_ncm', '');
  Result.CEST          := J.GetValue<string> ('cest',          '');
  Result.DescricaoCEST := J.GetValue<string> ('descricao_cest','');
  Result.Segmento      := J.GetValue<string> ('segmento',      '');
  Result.TemST         := J.GetValue<Boolean>('tem_st',        False);
  Result.NCMValidado   := J.GetValue<Boolean>('ncm_validado',  False);
  Result.NCMVigente    := J.GetValue<Boolean>('ncm_vigente',   False);
  Result.Confianca     := ParseConfianca(J.GetValue<string>('confianca', ''));
  Result.Justificativa := J.GetValue<string> ('justificativa', '');
  Result.Fonte         := J.GetValue<string> ('fonte',         '');
end;

// =========================== Endpoints =========================== //

function TNcmAPI.BuscarNCM(const Produto: string): TNcmResultado;
var
  Body: TJSONObject;
  J: TJSONValue;
begin
  Body := TJSONObject.Create;
  J := nil;
  try
    Body.AddPair('produto', Produto);
    J := Request('POST', '/ncm/buscar', Body);
    Result := ParseResultado(J as TJSONObject);
  finally
    Body.Free;
    J.Free;
  end;
end;

function TNcmAPI.BuscarBatch(const Produtos: TArray<string>): TArray<TNcmResultado>;
var
  Body: TJSONObject;
  Arr: TJSONArray;
  Resp: TJSONValue;
  ListaJ: TJSONArray;
  i: Integer;
begin
  if Length(Produtos) = 0 then begin
    SetLength(Result, 0);
    Exit;
  end;
  if Length(Produtos) > 500 then
    raise ENcmAPIError.Create('Máximo de 500 produtos por requisição', 0);

  Body := TJSONObject.Create;
  Arr := TJSONArray.Create;
  Resp := nil;
  try
    for i := 0 to Length(Produtos) - 1 do
      Arr.AddElement(TJSONString.Create(Produtos[i]));
    Body.AddPair('produtos', Arr);

    Resp := Request('POST', '/ncm/batch', Body);
    if not (Resp is TJSONObject) then
      raise ENcmAPIError.Create('Resposta inesperada do batch', FLastStatus);

    ListaJ := (Resp as TJSONObject).GetValue<TJSONArray>('resultado');
    SetLength(Result, ListaJ.Count);
    for i := 0 to ListaJ.Count - 1 do
      Result[i] := ParseResultado(ListaJ.Items[i] as TJSONObject);
  finally
    Body.Free;
    Resp.Free;
  end;
end;

function TNcmAPI.ObterNCM(const Codigo: string): TNcmDetalhe;
var
  Resp: TJSONValue;
  J: TJSONObject;
  CestArr: TJSONArray;
  CestObj: TJSONObject;
  i: Integer;
  S: string;
begin
  FillChar(Result, SizeOf(Result), 0);
  Resp := nil;
  try
    try
      Resp := Request('GET', '/ncm/' + Codigo);
    except
      on E: ENcmAPINotFound do begin
        Result.Sucesso := False;
        Result.Erro    := 'NCM não encontrado';
        Exit;
      end;
    end;
    J := Resp as TJSONObject;
    Result.Sucesso   := True;
    Result.Codigo    := J.GetValue<string> ('codncm',    '');
    Result.Descricao := J.GetValue<string> ('descricao', '');
    Result.Vigente   := J.GetValue<Boolean>('vigente',   False);
    Result.TemST     := J.GetValue<Boolean>('tem_st',    False);
    S := J.GetValue<string>('validade_ini', '');
    if S <> '' then
      try Result.ValidadeIni := ISO8601ToDate(S, False); except end;
    S := J.GetValue<string>('validade_fim', '');
    if S <> '' then
      try Result.ValidadeFim := ISO8601ToDate(S, False); except end;

    CestArr := J.GetValue<TJSONArray>('cests');
    if Assigned(CestArr) then
    begin
      SetLength(Result.Cests, CestArr.Count);
      for i := 0 to CestArr.Count - 1 do
      begin
        CestObj := CestArr.Items[i] as TJSONObject;
        Result.Cests[i].Codigo    := CestObj.GetValue<string>('codcest',   '');
        Result.Cests[i].Descricao := CestObj.GetValue<string>('descricao', '');
        Result.Cests[i].Segmento  := CestObj.GetValue<string>('segmento',  '');
      end;
    end;
  finally
    Resp.Free;
  end;
end;

function TNcmAPI.ObterTributacao(const Codigo, UF: string;
  Regime: TRegime; const CfopInterno, CfopExterno: string;
  Atividade: TAtividade): TTributacao;
var
  Path, AtividadeStr: string;
  Resp: TJSONValue;
  J, Sub, AnalisadosObj, CfopObj: TJSONObject;
begin
  FillChar(Result, SizeOf(Result), 0);
  Path := Format('/ncm/%s/tributacao?uf=%s&regime=%s',
                 [Codigo, UF, RegimeToStr(Regime)]);
  if CfopInterno <> '' then
    Path := Path + '&cfop_interno=' + TNetEncoding.URL.Encode(CfopInterno);
  if CfopExterno <> '' then
    Path := Path + '&cfop_externo=' + TNetEncoding.URL.Encode(CfopExterno);
  AtividadeStr := AtividadeToStr(Atividade);
  if AtividadeStr <> '' then
    Path := Path + '&atividade=' + AtividadeStr;
  Resp := nil;
  try
    Resp := Request('GET', Path);
    J := Resp as TJSONObject;
    Result.Sucesso := True;
    Result.NCM    := J.GetValue<string> ('ncm', '');
    Result.UF     := J.GetValue<string> ('uf', '');
    Result.Regime := J.GetValue<string> ('regime', '');
    Result.TemST  := J.GetValue<Boolean>('tem_st', False);
    Result.CEST   := J.GetValue<string> ('cest', '');

    Sub := J.GetValue<TJSONObject>('regime_atual');
    if Assigned(Sub) then
    begin
      // ICMS — alíquota nominal + alíquota destacada na NF-e + flag de decisão
      CfopObj := Sub.GetValue<TJSONObject>('icms');
      if Assigned(CfopObj) then
      begin
        Result.AliqICMS              := CfopObj.GetValue<Double> ('aliquota', 0);
        Result.AliqICMSDestacadaNfe  := CfopObj.GetValue<Double> ('aliquota_destacada_nfe', 0);
        Result.CstICMS               := CfopObj.GetValue<string> ('cst', '');
        // Default TRUE pra compatibilidade com versões antigas da API (sem a flag)
        Result.IcmsDestacarNfe       := CfopObj.GetValue<Boolean>('destacar_na_nfe', True);
        Result.IcmsObs               := CfopObj.GetValue<string> ('obs', '');
      end;
      // PIS
      CfopObj := Sub.GetValue<TJSONObject>('pis');
      if Assigned(CfopObj) then
      begin
        Result.AliqPIS             := CfopObj.GetValue<Double> ('aliquota', 0);
        Result.AliqPISDestacadaNfe := CfopObj.GetValue<Double> ('aliquota_destacada_nfe', 0);
        Result.CstPIS              := CfopObj.GetValue<string> ('cst', '');
        Result.PisDestacarNfe      := CfopObj.GetValue<Boolean>('destacar_na_nfe', True);
        Result.PisObs              := CfopObj.GetValue<string> ('obs', '');
      end;
      // COFINS
      CfopObj := Sub.GetValue<TJSONObject>('cofins');
      if Assigned(CfopObj) then
      begin
        Result.AliqCOFINS             := CfopObj.GetValue<Double> ('aliquota', 0);
        Result.AliqCOFINSDestacadaNfe := CfopObj.GetValue<Double> ('aliquota_destacada_nfe', 0);
        Result.CstCOFINS              := CfopObj.GetValue<string> ('cst', '');
        Result.CofinsDestacarNfe      := CfopObj.GetValue<Boolean>('destacar_na_nfe', True);
        Result.CofinsObs              := CfopObj.GetValue<string> ('obs', '');
      end;
      // IPI
      CfopObj := Sub.GetValue<TJSONObject>('ipi');
      if Assigned(CfopObj) then
      begin
        Result.AliqIPI         := CfopObj.GetValue<Double> ('aliquota', 0);
        Result.CstIPI          := CfopObj.GetValue<string> ('cst', '');
        Result.IpiDestacarNfe  := CfopObj.GetValue<Boolean>('destacar_na_nfe', True);
      end;
    end;

    Sub := J.GetValue<TJSONObject>('ibpt');
    if Assigned(Sub) then
    begin
      Result.IbptFedNac     := Sub.GetValue<Double>('fed_nacional', 0);
      Result.IbptFedImp     := Sub.GetValue<Double>('fed_importado', 0);
      Result.IbptEstadual   := Sub.GetValue<Double>('estadual', 0);
      Result.IbptMunicipal  := Sub.GetValue<Double>('municipal', 0);
      Result.IbptCargaTotal := Sub.GetValue<Double>('carga_total', 0);
      Result.IbptVersao     := Sub.GetValue<string>('versao', '');
    end;

    Sub := J.GetValue<TJSONObject>('reforma_tributaria');
    if Assigned(Sub) then
    begin
      Result.AnoReforma  := Sub.GetValue<Integer>('ano_referencia', 0);
      Result.TotalIBSCBS := Sub.GetValue<Double> ('total_ibs_cbs', 0);
      if Sub.GetValue('cbs') <> nil then
        Result.AliqCBS := (Sub.GetValue('cbs') as TJSONObject).GetValue<Double>('aliquota_efetiva', 0);
      if Sub.GetValue('ibs') <> nil then
        Result.AliqIBS := (Sub.GetValue('ibs') as TJSONObject).GetValue<Double>('aliquota_efetiva', 0);
      if Sub.GetValue('is')  <> nil then begin
        Result.TemIS  := (Sub.GetValue('is') as TJSONObject).GetValue<Boolean>('tem_is', False);
        Result.AliqIS := (Sub.GetValue('is') as TJSONObject).GetValue<Double> ('aliquota', 0);
      end;
    end;

    Sub := J.GetValue<TJSONObject>('cst_sugerido');
    if Assigned(Sub) then
      Result.CstIbsCbs := Sub.GetValue<string>('ibs_cbs', '');

    Sub := J.GetValue<TJSONObject>('cfop_sugerido');
    if Assigned(Sub) then
    begin
      Result.CfopInterna   := Sub.GetValue<string>('venda_interna', '');
      Result.CfopInteresta := Sub.GetValue<string>('venda_interestadual', '');
    end;

    // Análise dos CFOPs informados (opcional)
    AnalisadosObj := J.GetValue<TJSONObject>('cfops_analisados');
    if Assigned(AnalisadosObj) then
    begin
      CfopObj := AnalisadosObj.GetValue<TJSONObject>('interno');
      if Assigned(CfopObj) then
      begin
        Result.CfopInternoAnalisado.Informado    := True;
        Result.CfopInternoAnalisado.Conhecido    := CfopObj.GetValue<Boolean>('conhecido', False);
        Result.CfopInternoAnalisado.Descricao    := CfopObj.GetValue<string> ('descricao', '');
        Result.CfopInternoAnalisado.TipoOperacao := CfopObj.GetValue<string> ('tipo_operacao', '');
        Result.CfopInternoAnalisado.TemST        := CfopObj.GetValue<Boolean>('tem_st', False);
        Result.CfopInternoAnalisado.CstIcmsSug   := CfopObj.GetValue<string> ('cst_icms_sugerido', '');
        Result.CfopInternoAnalisado.Alerta       := CfopObj.GetValue<string> ('alerta', '');
      end;
      CfopObj := AnalisadosObj.GetValue<TJSONObject>('externo');
      if Assigned(CfopObj) then
      begin
        Result.CfopExternoAnalisado.Informado    := True;
        Result.CfopExternoAnalisado.Conhecido    := CfopObj.GetValue<Boolean>('conhecido', False);
        Result.CfopExternoAnalisado.Descricao    := CfopObj.GetValue<string> ('descricao', '');
        Result.CfopExternoAnalisado.TipoOperacao := CfopObj.GetValue<string> ('tipo_operacao', '');
        Result.CfopExternoAnalisado.TemST        := CfopObj.GetValue<Boolean>('tem_st', False);
        Result.CfopExternoAnalisado.CstIcmsSug   := CfopObj.GetValue<string> ('cst_icms_sugerido', '');
        Result.CfopExternoAnalisado.Alerta       := CfopObj.GetValue<string> ('alerta', '');
      end;
    end;

    Result.Raw := (Resp as TJSONObject).Clone as TJSONObject;
  finally
    Resp.Free;
  end;
end;

function TNcmAPI.ObterCEST(const NCM: string): TArray<TCestInfo>;
var
  Resp: TJSONValue;
  Arr: TJSONArray;
  Obj: TJSONObject;
  i: Integer;
begin
  SetLength(Result, 0);
  Resp := nil;
  try
    Resp := Request('GET', '/cest/' + NCM);
    Arr := (Resp as TJSONObject).GetValue<TJSONArray>('cests');
    if not Assigned(Arr) then Exit;
    SetLength(Result, Arr.Count);
    for i := 0 to Arr.Count - 1 do
    begin
      Obj := Arr.Items[i] as TJSONObject;
      Result[i].Codigo    := Obj.GetValue<string>('codcest',   '');
      Result[i].Descricao := Obj.GetValue<string>('descricao', '');
      Result[i].Segmento  := Obj.GetValue<string>('segmento',  '');
    end;
  finally
    Resp.Free;
  end;
end;

function TNcmAPI.ObterSaldo: TSaldo;
var
  Resp: TJSONValue;
  J, Obj: TJSONObject;
begin
  FillChar(Result, SizeOf(Result), 0);
  Resp := nil;
  try
    try
      Resp := Request('GET', '/quota');
    except
      on E: ENcmAPIError do
      begin
        Result.Sucesso := False;
        Result.Erro    := E.Message;
        Exit;
      end;
    end;
    J := Resp as TJSONObject;
    Result.Sucesso := True;

    Obj := J.GetValue<TJSONObject>('tenant');
    if Assigned(Obj) then
    begin
      Result.TenantNome      := Obj.GetValue<string>('nome', '');
      Result.TenantAtividade := Obj.GetValue<string>('atividade', '');
    end;
    Obj := J.GetValue<TJSONObject>('api_key');
    if Assigned(Obj) then
      Result.Ambiente := Obj.GetValue<string>('ambiente', 'producao');

    Obj := J.GetValue<TJSONObject>('assinatura');
    if Assigned(Obj) then
    begin
      Result.PlanoNome          := Obj.GetValue<string> ('plano', '');
      Result.VigenciaFim        := Obj.GetValue<string> ('vigencia_fim', '');
      Result.DiasParaVencer     := Obj.GetValue<Integer>('dias_para_vencer', 0);
      Result.AssinaturaVencida  := Obj.GetValue<Boolean>('vencida', False);
    end;

    Obj := J.GetValue<TJSONObject>('saldo');
    if Assigned(Obj) then
    begin
      Result.SaldoTotal           := Obj.GetValue<Integer>('total', 0);
      Result.SaldoUsado           := Obj.GetValue<Integer>('usado', 0);
      Result.SaldoDisponivel      := Obj.GetValue<Integer>('disponivel', 0);
      Result.PercentualUsado      := Obj.GetValue<Double> ('percentual_usado', 0);
      Result.PercentualDisponivel := Obj.GetValue<Double> ('percentual_disponivel', 0);
      Result.CreditosExtras       := Obj.GetValue<Integer>('creditos_extras', 0);
    end;

    Obj := J.GetValue<TJSONObject>('custos');
    if Assigned(Obj) then
    begin
      Result.CustoNcmBuscar  := Obj.GetValue<Integer>('ncm_buscar_por_produto', 1);
      Result.CustoNcmBatch   := Obj.GetValue<Integer>('ncm_batch_por_produto', 1);
      Result.CustoTributacao := Obj.GetValue<Integer>('ncm_tributacao', 0);
      Result.CustoCestGet    := Obj.GetValue<Integer>('cest_get', 0);
    end;

    Result.HomologacaoGratis := J.GetValue<Boolean>('homologacao_gratis', False);
  finally
    Resp.Free;
  end;
end;

function TNcmAPI.TestarConexao: Boolean;
var
  Resp: IHTTPResponse;
begin
  try
    Resp := FHttp.Get(FBaseURL + '/health');
    Result := (Resp.StatusCode = 200);
  except
    Result := False;
  end;
end;

end.
