-- 0045 -- paineis: o grafico que fica na tela, sem custar uma pergunta
--
-- O PROBLEMA
--
-- O assistente ja desenha grafico, mas so DENTRO de uma conversa e so quando o
-- modelo decide chamar `mostrar_grafico`. Disso saem tres consequencias:
--
--   nao se reabre     o grafico de ontem so volta perguntando de novo -- e
--                     perguntar de novo e outra chamada de LLM, que pode
--                     devolver outro numero;
--   nao se acompanha  nada fica na tela; quem quer ver inadimplencia toda
--                     segunda-feira reescreve a pergunta toda segunda-feira;
--   custa por olhada  cada abertura e o laco inteiro do agente -- medido em 7-8
--                     voltas em metade das perguntas, com custo superlinear
--                     porque cada volta reenvia o contexto acumulado.
--
-- Um painel resolve os tres: e montado uma vez, atualiza sozinho e a
-- atualizacao nao passa por modelo nenhum.
--
--
-- A DECISAO QUE GOVERNA ESTA TABELA: O PAINEL GUARDA UMA CONSULTA, NAO UMA
-- PERGUNTA
--
-- E o inverso da decisao de `embed_prompts`, e de proposito. La o item guarda
-- TEXTO, que entra no campo de pergunta e segue o mesmo caminho de uma pergunta
-- digitada -- porque o valor do menu e ensinar o que perguntar. Aqui o valor e
-- o oposto: o numero tem de ser o MESMO em duas aberturas seguidas, e tem de
-- sair sem cobrar ninguem.
--
-- Entao `spec` guarda uma consulta ESTRUTURADA -- dataset, dimensao, medida,
-- filtros, periodo -- que o servidor compila para SQL na hora de executar. Nao
-- e SQL congelado: SQL congelado nomeia a view de UM projeto, e o mesmo painel
-- da camada do tenant precisa rodar nos N projetos dele, cada um com a sua
-- view. O que fica congelado e a INTENCAO; o SQL nasce a cada execucao, ja
-- recortado pelo alcance da sessao, e passa por `datasetAllowlist` e `SqlGuard`
-- como qualquer outra consulta. O painel nao ganha porta nova para os dados.
--
--
-- POR QUE `spec` E JSON
--
-- Mesma razao de `skills.requires`: o numero de filtros e variavel. Colunar
-- isso produziria `filtro_1_coluna`, `filtro_1_valor`, `filtro_2_coluna` -- e a
-- primeira consulta com tres filtros exigiria migracao.
--
-- O JSON NAO e texto livre: `PanelSpec` valida forma e conteudo na gravacao, e
-- toda coluna citada e conferida contra as colunas reais da view antes de
-- qualquer SQL existir. Valor de filtro nunca e interpolado -- vai como bind.
--
--
-- DUAS CAMADAS, COMO `embed_prompts` -- E NAO TRES, COMO `skills`
--
--   project_id = 7      o painel e daquele cliente
--   project_id IS NULL  o painel e do TENANT, herdado por todos os projetos
--
-- Nao ha camada OFICIAL aqui, e a ausencia e a informacao: uma skill oficial
-- pode existir porque declara GRUPO e TERMO, que sao portateis entre ERPs. Um
-- painel nomeia dataset e coluna -- `contas_receber.vencimento` -- e isso e o
-- vocabulario do ERP de um cliente. Um painel oficial nasceria quebrado em todo
-- cliente cujo ERP chamou aquilo de outra coisa, em silencio.
--
-- Sobrescrita por (grupo, rotulo) e opt-out por projeto: identico a 0032, pelas
-- mesmas razoes, que estao escritas por extenso la.
--
--
-- `agent_slug`, E NAO `agent_id`
--
-- A razao ja esta escrita em EmbedConfig::$defaultAgent: o id aponta para UMA
-- LINHA. Se o cliente sobrescreve um agente oficial criando um com o mesmo slug
-- na camada dele -- que e exatamente como a sobrescrita funciona --, um painel
-- preso por id continuaria analisando com a postura oficial, e o override que
-- ele acabou de escrever nao valeria.
--
--
-- O QUE ESTA TABELA NAO GUARDA
--
-- Nao guarda resultado. O numero e lido da view a cada execucao, e a view e a
-- verdade. Guardar o resultado criaria a pergunta "de quando e este numero?" em
-- toda tela, e uma invalidacao para acertar -- que custaria mais que reexecutar
-- uma consulta agregada.
--
-- Guarda, sim, a ANALISE -- em `embed_panel_analyses` -- porque ali o custo e
-- real: analise e chamada de modelo. Ver o cabecalho daquela tabela.

CREATE TABLE embed_panels (
  id             BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,

  -- A barreira. Primeira condicao de todo WHERE, inclusive na leitura herdada.
  tenant_id      BIGINT UNSIGNED NOT NULL,

  -- NULL = camada do TENANT, herdada por todos os projetos.
  project_id     BIGINT UNSIGNED NULL,

  -- Existe para a UNIQUE alcancar a camada do tenant. VIRTUAL: nao ocupa espaco.
  scope_project_id BIGINT UNSIGNED
    GENERATED ALWAYS AS (COALESCE(project_id, 0)) VIRTUAL,

  -- A secao da tela: "Financeiro", "Vendas". String, como em 0032 -- um grupo e
  -- um rotulo, nao uma entidade, e a ordem dos grupos sai do menor position.
  group_label    VARCHAR(120) NOT NULL,

  -- O titulo do cartao: "Inadimplencia por empresa".
  label          VARCHAR(160) NOT NULL,

  -- Uma linha de explicacao, mostrada ao usuario final sob o titulo.
  hint           VARCHAR(255) NULL,

  -- Como desenhar: bar | line | pie | indicador | tabela.
  --
  -- Os tres primeiros sao os mesmos do grafico do chat, e deliberadamente
  -- poucos pela mesma razao: cada tipo a mais e um caminho a mais para escolher
  -- errado. `indicador` e o numero unico -- metade das perguntas tem resposta
  -- unica -- e `tabela` e o que nao cabe em eixo nenhum.
  chart          VARCHAR(16) NOT NULL DEFAULT 'bar',

  -- A consulta estruturada. Ver PanelSpec.
  spec           JSON NOT NULL,

  -- A postura da analise. NULL = painel sem analise, e e o padrao: analise
  -- custa chamada de modelo, e a maioria dos paineis so precisa do numero.
  agent_slug     VARCHAR(80) NULL,

  -- Ordem DENTRO do grupo. A dos grupos e derivada, como em 0032.
  position       INT NOT NULL DEFAULT 0,

  -- Desliga a linha NA CAMADA. Para esconder um HERDADO em um projeto so, o
  -- instrumento e embed_panel_optouts.
  enabled        TINYINT(1) NOT NULL DEFAULT 1,

  -- Por que esta desligado. Visao administrativa; nunca sai para o usuario.
  note           VARCHAR(255) NULL,

  created_by     VARCHAR(120) NULL,
  created_at     DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at     DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

  PRIMARY KEY (id),

  -- As duas camadas protegidas pela MESMA chave, via a coluna gerada.
  UNIQUE KEY uq_embed_panels_camada (tenant_id, scope_project_id, group_label, label),

  -- A FK para projects precisa de indice comecando por project_id, e a UNIQUE
  -- acima nem menciona a coluna (usa a gerada).
  KEY idx_embed_panels_project (project_id),

  -- Serve a leitura da tela: tenant + camada + ligados, na ordem de exibicao.
  KEY idx_embed_panels_tela (tenant_id, project_id, enabled, position),

  CONSTRAINT fk_embed_panels_tenant  FOREIGN KEY (tenant_id)  REFERENCES tenants(id),
  CONSTRAINT fk_embed_panels_project FOREIGN KEY (project_id) REFERENCES projects(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- Esconder um painel HERDADO em um projeto so, sem apagar para os outros.
-- Tabela e nao coluna: quem desliga e o PAR (projeto, painel). Ver 0032.
CREATE TABLE embed_panel_optouts (
  id          BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  project_id  BIGINT UNSIGNED NOT NULL,
  panel_id    BIGINT UNSIGNED NOT NULL,
  created_at  DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,

  PRIMARY KEY (id),
  UNIQUE KEY uq_embed_panel_optouts (project_id, panel_id),
  KEY idx_embed_panel_optouts_panel (panel_id),

  CONSTRAINT fk_embed_panel_optouts_project FOREIGN KEY (project_id)
    REFERENCES projects(id),
  -- CASCADE: apagado o painel, o opt-out dele nao significa mais nada.
  CONSTRAINT fk_embed_panel_optouts_panel FOREIGN KEY (panel_id)
    REFERENCES embed_panels(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- ---------------------------------------------------------------------------
-- O CACHE DA ANALISE
-- ---------------------------------------------------------------------------
-- A analise e a leitura que um agente faz do numero apurado. Ela custa uma
-- chamada de modelo -- e e por isso que ela, ao contrario do resultado, e
-- guardada.
--
-- A CHAVE E A IMPRESSAO DIGITAL DO RESULTADO, e isso e o desenho inteiro:
-- enquanto o numero for o mesmo, a analise continua valendo e ninguem paga de
-- novo; no instante em que o dado muda, a chave muda junto e a analise velha
-- simplesmente nao e encontrada. Nao ha invalidacao para escrever, e nao ha o
-- pior desfecho possivel aqui -- comentario velho ao lado de numero novo, que e
-- pior que comentario nenhum porque parece atual.
--
-- `agent_slug` entra na chave porque a mesma tabela lida por posturas
-- diferentes rende leituras diferentes, e as duas sao legitimas.
--
-- `tenant_id` entra NOT NULL mesmo havendo `panel_id`. E a barreira, nao
-- conveniencia -- o mesmo motivo escrito em 0032: um painel da camada do tenant
-- roda em N projetos, e a leitura do cache tem de ser incapaz de atravessar
-- para outro cliente mesmo que alguem escreva o WHERE errado.
CREATE TABLE embed_panel_analyses (
  id           BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  panel_id     BIGINT UNSIGNED NOT NULL,
  tenant_id    BIGINT UNSIGNED NOT NULL,

  -- Qual postura escreveu. String vazia nunca: sem agente nao ha analise.
  agent_slug   VARCHAR(80) NOT NULL,

  -- SHA-256 do resultado ja apurado (rotulos + series + unidade). Hex, 64.
  fingerprint  CHAR(64) NOT NULL,

  body         TEXT NOT NULL,

  -- O consumo desta analise. Serve a mesma conta que as metricas da conversa.
  tokens_in    INT UNSIGNED NOT NULL DEFAULT 0,
  tokens_out   INT UNSIGNED NOT NULL DEFAULT 0,

  created_at   DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,

  PRIMARY KEY (id),
  UNIQUE KEY uq_embed_panel_analysis (panel_id, tenant_id, agent_slug, fingerprint),
  KEY idx_embed_panel_analyses_tenant (tenant_id),

  CONSTRAINT fk_embed_panel_analyses_panel FOREIGN KEY (panel_id)
    REFERENCES embed_panels(id) ON DELETE CASCADE,
  CONSTRAINT fk_embed_panel_analyses_tenant FOREIGN KEY (tenant_id)
    REFERENCES tenants(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
