package db

import (
	"context"
	"fmt"
	"regexp"
	"strings"
	"unicode"

	"github.com/jackc/pgx/v5/pgxpool"
	"golang.org/x/text/unicode/norm"
)

var produtoRe = regexp.MustCompile(`^[a-z_][a-z0-9_]*$`)

// TableNameValido valida se o nome de tabela está no formato esperado do projeto:
// "nps_" + produto normalizado.
//
// Motivação (segurança): identificadores não podem ser parametrizados em SQL.
// Então, sempre que precisarmos interpolar um nome de tabela, validamos aqui
// para evitar SQL injection via identificador.
func TableNameValido(tableName string) bool {
	if !strings.HasPrefix(tableName, "nps_") {
		return false
	}
	produto := strings.TrimPrefix(tableName, "nps_")
	// produtoRe já garante: ^[a-z_][a-z0-9_]*$
	// (e a normalização limita tamanho em NormalizeProduto).
	return produtoRe.MatchString(produto)
}

// NormalizeProduto normaliza e valida um nome de produto para uso em:
// - nomes de tabela no Postgres (prefixo nps_)
// - rotas/URLs (parâmetro {produto})
//
// Regras:
// - minúsculo + trim
// - remove diacríticos
// - converte qualquer caractere fora de [a-z0-9_] para '_'
// - colapsa '_' repetidos
// - valida contra regex e tamanho máximo de identificador
//
// Importante: isso NÃO é usado para exibição ao usuário.
func NormalizeProduto(produtoNome string) (string, error) {
	p := strings.ToLower(strings.TrimSpace(produtoNome))
	if p == "" {
		return "", fmt.Errorf("produto invalido")
	}

	// Remove diacritics (NFD + strip marks)
	p = norm.NFD.String(p)
	p = strings.Map(func(r rune) rune {
		if unicode.Is(unicode.Mn, r) {
			return -1
		}
		return r
	}, p)

	// Replace anything not allowed with underscore
	p = strings.Map(func(r rune) rune {
		switch {
		case r >= 'a' && r <= 'z':
			return r
		case r >= '0' && r <= '9':
			return r
		case r == '_':
			return r
		default:
			return '_'
		}
	}, p)

	// Collapse underscores
	for strings.Contains(p, "__") {
		p = strings.ReplaceAll(p, "__", "_")
	}
	p = strings.Trim(p, "_")

	// Postgres identifiers are max 63 chars. Table name is "nps_" + produto.
	if len(p) > 59 {
		return "", fmt.Errorf("produto invalido")
	}

	if !produtoRe.MatchString(p) {
		return "", fmt.Errorf("produto invalido")
	}
	return p, nil
}

func TableNameForProduto(produto string) string {
	return "nps_" + produto
}

func EnsurePgcrypto(ctx context.Context, pool *pgxpool.Pool) error {
	_, err := pool.Exec(ctx, `CREATE EXTENSION IF NOT EXISTS pgcrypto`)
	return err
}

// EnsurePainelTables cria as tabelas globais usadas pelo painel.
//
// Motivação:
//   - as respostas ficam em tabelas dinâmicas por produto (nps_{produto})
//   - precisamos de um lugar único para registrar auditoria de análise (pendente/concluída)
//     e comentários internos do painel
//
// Importante:
// - tudo é criado de forma defensiva (IF NOT EXISTS)
// - SQL sempre parametrizado (aqui não há identificadores dinâmicos)
func EnsurePainelTables(ctx context.Context, pool *pgxpool.Pool) error {
	// Status de análise por (produto + resposta_id)
	if _, err := pool.Exec(ctx, `
CREATE TABLE IF NOT EXISTS painel_resposta_status (
  produto text NOT NULL,
  resposta_id text NOT NULL,
  status text NOT NULL CHECK (status IN ('pendente','concluida')),
  concluida_em timestamptz NULL,
  atualizado_em timestamptz NOT NULL DEFAULT now(),
  PRIMARY KEY (produto, resposta_id)
)`); err != nil {
		return err
	}

	// Comentários internos por resposta.
	if _, err := pool.Exec(ctx, `
CREATE TABLE IF NOT EXISTS painel_resposta_comentario (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  produto text NOT NULL,
  resposta_id text NOT NULL,
  pessoa_nome text NOT NULL,
  sessao_id uuid NOT NULL,
  comentario text NOT NULL,
  criado_em timestamptz NOT NULL DEFAULT now(),
  atualizado_em timestamptz NOT NULL DEFAULT now()
)`); err != nil {
		return err
	}

	if _, err := pool.Exec(ctx, `
CREATE INDEX IF NOT EXISTS idx_painel_resp_comentario
  ON painel_resposta_comentario (produto, resposta_id, criado_em ASC)
`); err != nil {
		return err
	}

	return nil
}

// EnsureNPSTable cria a tabela por produto + índices se não existirem.
// Importante: tableName deve ser criada a partir de um produto normalizado.
func EnsureNPSTable(ctx context.Context, pool *pgxpool.Pool, tableName string) error {
	// Identifiers cannot be passed as $1 parameters, so we must interpolate.
	// Segurança: tableName deve ser estritamente derivada de NormalizeProduto + prefix.
	if !TableNameValido(tableName) {
		return fmt.Errorf("nome de tabela invalido")
	}
	q := fmt.Sprintf(`
CREATE TABLE IF NOT EXISTS %s (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  -- Nome do produto como informado pela integração/widget.
  -- Importante: NÃO é usado para nome de tabela; é apenas para exibição.
  produto_nome text NOT NULL DEFAULT '',
  inquilino_codigo text NOT NULL,
  inquilino_nome text NOT NULL,
  usuario_codigo text,
  usuario_nome text NOT NULL,
  usuario_email text,
  usuario_telefone text,
  status text NOT NULL CHECK (status IN ('pedido','respondido')),
  pedido_criado_em timestamptz NOT NULL DEFAULT now(),
  respondido_em timestamptz NULL,
  atualizado_em timestamptz NOT NULL DEFAULT now(),
  -- Escala NPS do projeto: 0–10.
  nota int NULL CHECK (nota BETWEEN 0 AND 10),
  justificativa text NULL,
  valida bool NOT NULL DEFAULT true,
  origem text NOT NULL DEFAULT 'widget_iframe',
  user_agent text NULL,
  -- IP real do usuário (após middleware RealIP). Pode conter IPv4 ou IPv6.
  -- Importante: quando rodar atrás de proxy (ex.: Docker + Nginx/Traefik),
  -- garanta que o proxy repasse X-Forwarded-For/X-Real-IP.
  ip_real text NULL
);

ALTER TABLE %s ADD COLUMN IF NOT EXISTS usuario_codigo text;
ALTER TABLE %s ADD COLUMN IF NOT EXISTS produto_nome text NOT NULL DEFAULT '';
ALTER TABLE %s ADD COLUMN IF NOT EXISTS ip_real text;

-- Migração defensiva (em runtime) da constraint de nota.
--
-- Motivação:
-- - Em versões anteriores a escala era 1–10.
-- - As tabelas por produto são criadas automaticamente; portanto, podem existir
--   tabelas antigas com CHECK antigo em nota.
--
-- Estratégia:
-- - Remover qualquer CHECK existente que mencione a coluna nota.
-- - Recriar uma constraint nomeada ck_nota_0_10 com a regra atual (0–10).
--
-- Segurança: tableName é validado por TableNameValido (regex) antes de ser
-- interpolado e usado como regclass/identificador.
DO $$
DECLARE c record;
BEGIN
  FOR c IN
    SELECT conname
    FROM pg_constraint
    WHERE conrelid = '%s'::regclass
      AND contype='c'
      AND pg_get_constraintdef(oid) ILIKE '%%nota%%'
  LOOP
    EXECUTE format('ALTER TABLE %%I DROP CONSTRAINT %%I', '%s', c.conname);
  END LOOP;

  IF NOT EXISTS (
    SELECT 1
    FROM pg_constraint
    WHERE conrelid = '%s'::regclass
      AND contype='c'
      AND conname='ck_nota_0_10'
  ) THEN
    EXECUTE format('ALTER TABLE %%I ADD CONSTRAINT ck_nota_0_10 CHECK (nota BETWEEN 0 AND 10)', '%s');
  END IF;
END$$;

-- NOTE: controle de exibição é por (produto + inquilino_codigo + usuario_codigo)
-- então os índices são baseados em usuario_codigo.

CREATE INDEX IF NOT EXISTS idx_nps_resp_recente_%s
  ON %s (inquilino_codigo, usuario_codigo, respondido_em DESC)
  WHERE status='respondido' AND valida=true;

CREATE INDEX IF NOT EXISTS idx_nps_pedido_aberto_%s
  ON %s (inquilino_codigo, usuario_codigo, pedido_criado_em DESC)
  WHERE status='pedido';
`,
		tableName, // CREATE TABLE
		tableName, // ALTER TABLE add usuario_codigo
		tableName, // ALTER TABLE add produto_nome
		tableName, // ALTER TABLE add ip_real
		tableName, // DO block: conrelid (1)
		tableName, // DO block: DROP CONSTRAINT (identificador)
		tableName, // DO block: conrelid (2)
		tableName, // DO block: ADD CONSTRAINT (identificador)
		tableName, // idx_nps_resp_recente_%s
		tableName, // ON %s
		tableName, // idx_nps_pedido_aberto_%s
		tableName, // ON %s
	)

	_, err := pool.Exec(ctx, q)
	return err
}
