import {
  Component,
  ChangeDetectorRef,
  Inject,
  PLATFORM_ID,
  AfterViewInit,
  OnInit,
  ViewChild,
  ElementRef,
  OnDestroy,
  HostBinding
} from '@angular/core';
import { isPlatformBrowser, CommonModule } from '@angular/common';
import { HttpClient } from '@angular/common/http';
import { ActivatedRoute, Router } from '@angular/router';
import Chart from 'chart.js/auto';
import type { ChartConfiguration, ChartData, ScriptableContext, TooltipItem } from 'chart.js';
import {
  ResumoService,
  ResumoResponse,
  MacrophonyPlan,
  MacrophonyTotals,
  VivoLineResumo,
  VivoLineTotals,
  ClienteEspecial,
  PlanoContratoResumo,
  PlanoContratoTotal,
  LineTotal,
  GbDistribuicao,
  ReservaLine,
  ReservaPorDdd,
  ReservaTotal
} from '../../services/resumo.service';
import { environment } from '../../../environments/environment';

type ResumoTab = 'planos' | 'clientes' | 'totais' | 'reserva';

// Configurações de UI
const ANIMATION_DELAY = 60;
const CHART_THEME = {
  brand: '#e33dcf',
  brandLight: '#fce7f9',
  blue: '#030faa',
  blueLight: '#eef2ff',
  success: '#10b981',
  fontFamily: "'Inter', sans-serif"
};

// ... (Interfaces e Types mantidos sem alteração na estrutura de dados) ...
// Para brevidade, mantive as definições de Tipos iguais ao original, focando nas mudanças visuais da classe.
type TableColumn<T> = {
  key: string;
  label: string;
  type?: 'text' | 'number' | 'money' | 'gb';
  align?: 'left' | 'right' | 'center';
  sortable?: boolean;
  value: (row: T) => any;
  display?: (value: any, row: T) => string;
  tone?: boolean;
  badge?: boolean;
  icon?: (row: T) => string | null;
  title?: (row: T) => string;
};
// ... (Demais tipos TableView, TableState, GroupedTableState mantidos) ...
type TableView<T> = { rows: T[]; total: number; totalPages: number; pageStart: number; pageEnd: number; pageNumbers: number[]; sorted: T[]; };
type TableState<T> = { key: string; label: string; data: T[]; columns: TableColumn<T>[]; search: string; page: number; pageSize: number; pageSizeOptions: number[]; sortKey: string; sortDir: 'asc' | 'desc'; compact: boolean; view: TableView<T> | null; };
type MacrophonyGroup = { key: string; plano: string; gbLabel: string; totalLinhas: number; valorTotal: number; valorUnitMedio: number | null; rows: MacrophonyPlan[]; };
type GroupMetric = { label: string; value: string; tone?: string };
type GroupItem<T> = { key: string; title: string; subtitle?: string; rows: T[]; metrics: GroupMetric[]; };
type GroupedTableState<T> = { key: string; label: string; table: TableState<T>; groupBy: (row: T) => string; groupTitle: (rows: T[]) => string; groupSubtitle?: (rows: T[]) => string | undefined; groupMetrics: (rows: T[]) => GroupMetric[]; groupSort?: (a: GroupItem<T>, b: GroupItem<T>) => number; search: string; page: number; pageSize: number; pageSizeOptions: number[]; compact: boolean; open: Set<string>; groups: GroupItem<T>[]; filtered: GroupItem<T>[]; view: GroupItem<T>[]; detailOpen: boolean; detailGroup: GroupItem<T> | null; };
type GeralLineTotalPayload = { tipo?: unknown; qtdLinhas?: unknown; valorTotalLine?: unknown; lucroTotalLine?: unknown; };

@Component({
  standalone: true,
  imports: [CommonModule],
  templateUrl: './resumo.html',
  styleUrls: ['./resumo.scss']
})
export class Resumo implements OnInit, AfterViewInit, OnDestroy {
  @HostBinding('class.animate-ready') animateReady = false;

  // Controle rápido para ocultar/exibir informações financeiras (valores/lucro).
  // Deixe false para não expor faturamento no Resumo/Dashboard; mude para true se o cliente solicitar no futuro.
  readonly showFinancial = false;

  loading = false;
  errorMessage = '';
  resumo: ResumoResponse | null = null;

  activeTab: ResumoTab = 'planos';
  readonly tabs = [
    { key: 'planos' as ResumoTab, label: 'Planos', icon: 'bi bi-collection' },
    { key: 'clientes' as ResumoTab, label: 'Clientes', icon: 'bi bi-people' },
    { key: 'totais' as ResumoTab, label: 'Totais Line', icon: 'bi bi-bar-chart' },
    { key: 'reserva' as ResumoTab, label: 'Reserva', icon: 'bi bi-box-seam' },
  ];

  @ViewChild('chartPlanos') chartPlanosRef?: ElementRef<HTMLCanvasElement>;
  @ViewChild('chartPlanosLinhas') chartPlanosLinhasRef?: ElementRef<HTMLCanvasElement>;
  @ViewChild('chartClientes') chartClientesRef?: ElementRef<HTMLCanvasElement>;
  @ViewChild('chartTotais') chartTotaisRef?: ElementRef<HTMLCanvasElement>;
  @ViewChild('chartReserva') chartReservaRef?: ElementRef<HTMLCanvasElement>;

  private charts: { [key: string]: Chart | undefined } = {};
  private viewReady = false;
  private dataReady = false;
  private readonly baseApi: string;
  private totaisLineFromGeral: LineTotal[] = [];

  // Estados de Tabela e Grupo
  macrophonyGroups: MacrophonyGroup[] = [];
  macrophonyView: MacrophonyGroup[] = [];
  macrophonySearch = '';
  macrophonyPage = 1;
  macrophonyPageSize = 5;
  macrophonyPageOptions = [5, 10, 20];
  macrophonyCompact = false;
  macrophonyOpen: Set<string> = new Set();
  macrophonyDetailOpen = false;
  macrophonyDetailGroup: MacrophonyGroup | null = null;
  private macrophonyFiltered: MacrophonyGroup[] = [];

  tableMacrophony!: TableState<MacrophonyPlan>;
  tablePlanoContrato!: TableState<PlanoContratoResumo>;
  tableClientes!: TableState<VivoLineResumo>;
  tableClientesEspeciais!: TableState<ClienteEspecial>;
  tableTotaisLine!: TableState<LineTotal>;
  tableReserva!: TableState<ReservaLine>;

  groupPlanoContrato!: GroupedTableState<PlanoContratoResumo>;
  groupClientes!: GroupedTableState<VivoLineResumo>;
  groupClientesEspeciais!: GroupedTableState<ClienteEspecial>;
  groupTotaisLine!: GroupedTableState<LineTotal>;
  groupReserva!: GroupedTableState<ReservaLine>;

  constructor(
    @Inject(PLATFORM_ID) private platformId: object,
    private http: HttpClient,
    private resumoService: ResumoService,
    private route: ActivatedRoute,
    private router: Router,
    private cdr: ChangeDetectorRef
  ) {
    const raw = (environment.apiUrl || '').replace(/\/+$/, '');
    this.baseApi = raw.toLowerCase().endsWith('/api') ? raw : `${raw}/api`;
    this.initTables();
    this.initGroupTables();
    // Default chart configuration for Enterprise look
    Chart.defaults.font.family = CHART_THEME.fontFamily;
    Chart.defaults.color = '#64748b';
    Chart.defaults.scale.grid.color = '#f1f5f9';
  }

  ngOnInit(): void {
    this.route.queryParamMap.subscribe((params) => {
      const tab = params.get('tab');
      if (this.isValidTab(tab)) {
        this.activeTab = tab as ResumoTab;
        this.deferChartBuild();
        this.deferAnimate();
      }
    });
    this.loadResumo();
  }

  ngAfterViewInit(): void {
    this.viewReady = true;
    this.animateReady = isPlatformBrowser(this.platformId);
    if (this.animateReady) this.animateIn();
    this.tryBuildCharts();
  }

  ngOnDestroy(): void {
    Object.values(this.charts).forEach(c => c?.destroy());
  }

  setTab(tab: ResumoTab): void {
    if (this.activeTab === tab) return;
    this.activeTab = tab;
    this.router.navigate([], { queryParams: { tab }, queryParamsHandling: 'merge' });
    this.deferChartBuild();
    this.deferAnimate();
  }

  refresh(): void {
    this.loadResumo();
  }

  // --- Helpers de UI ---
  trackByIndex(index: number) { return index; }
  
  // Util para criar Gradiente no Chart.js
  private createGradient(ctx: CanvasRenderingContext2D, colorStart: string, colorEnd: string) {
    const gradient = ctx.createLinearGradient(0, 0, 0, 400);
    gradient.addColorStop(0, colorStart);
    gradient.addColorStop(1, colorEnd);
    return gradient;
  }

  // --- Lógica de Gráficos (Visual) ---
  private buildChartsForActiveTab(): void {
    if (!this.resumo) return;
    this.destroyCharts();

    if (this.activeTab === 'planos') {
      if (this.showFinancial) {
        this.buildChartPlanos();
      }
      this.buildChartPlanosLinhas();
    } else if (this.activeTab === 'clientes') {
      this.buildChartClientes();
    } else if (this.activeTab === 'totais') {
      this.buildChartTotais();
    } else if (this.activeTab === 'reserva') {
      this.buildChartReserva();
    }
  }

  private buildChartPlanos() {
    const canvas = this.chartPlanosRef?.nativeElement;
    if (!canvas) return;
    
    const data = this.macrophonyGroups
      .map(g => ({ label: g.plano, valor: g.valorTotal }))
      .sort((a, b) => b.valor - a.valor)
      .slice(0, 10);

    const ctx = canvas.getContext('2d');
    const bg = ctx ? this.createGradient(ctx, CHART_THEME.brand, '#b0249d') : CHART_THEME.brand;

    this.charts['planos'] = new Chart(canvas, {
      type: 'bar',
      data: {
        labels: data.map(d => d.label.length > 20 ? d.label.slice(0, 20) + '...' : d.label),
        datasets: [{
          label: 'Valor Total',
          data: data.map(d => d.valor),
          backgroundColor: bg,
          borderRadius: 4,
          barPercentage: 0.6,
          minBarLength: 8,
        }]
      },
      options: this.getCommonChartOptions('currency')
    });
  }

  private buildChartPlanosLinhas() {
    const canvas = this.chartPlanosLinhasRef?.nativeElement;
    if (!canvas) return;

    const data = this.macrophonyGroups
      .map(g => ({ label: g.plano, linhas: g.totalLinhas }))
      .sort((a, b) => b.linhas - a.linhas)
      .slice(0, 10);

    const ctx = canvas.getContext('2d');
    const bg = ctx ? this.createGradient(ctx, CHART_THEME.blue, '#2563eb') : CHART_THEME.blue;

    const common = this.getCommonChartOptions('number') as any;

    this.charts['planosLinhas'] = new Chart(canvas, {
      type: 'bar',
      data: {
        labels: data.map(d => d.label.length > 20 ? d.label.slice(0, 20) + '...' : d.label),
        datasets: [{
          label: 'Total Linhas',
          data: data.map(d => d.linhas),
          backgroundColor: bg,
          borderRadius: 4,
          barPercentage: 0.6,
          minBarLength: 8,
        }]
      },
      options: {
        ...common,
        plugins: {
          ...common.plugins,
          tooltip: {
            ...(common.plugins?.tooltip ?? {}),
            callbacks: {
              ...((common.plugins?.tooltip as any)?.callbacks ?? {}),
              label: (ctx: TooltipItem<'bar'>) => ` ${this.formatNumber(ctx.raw)} Linhas`,
            }
          }
        }
      }
    });
  }

  private buildChartClientes() {
    const canvas = this.chartClientesRef?.nativeElement;
    if (!canvas) return;

    const metricLabel = this.showFinancial ? 'Lucro Estimado' : 'Qtd. Linhas';
    const metricType = this.showFinancial ? 'currency' : 'number';

    const data = (this.resumo?.vivoLineResumos ?? [])
      .map(c => ({
        label: c.cliente,
        value: this.showFinancial ? (this.toNumber(c.lucro) ?? 0) : (this.toNumber(c.qtdLinhas) ?? 0),
      }))
      .sort((a, b) => b.value - a.value)
      .slice(0, 10);

    // Garante altura suficiente para exibir todos os nomes no eixo Y sem auto-skip.
    const parent = canvas.parentElement as HTMLElement | null;
    if (parent) {
      const minHeight = 300;
      const rowHeight = 34;
      parent.style.height = `${Math.max(minHeight, data.length * rowHeight)}px`;
    }

    const common = this.getCommonChartOptions(metricType) as any;
    const plugins =
      metricType === 'number'
        ? {
            ...common.plugins,
            tooltip: {
              ...(common.plugins?.tooltip ?? {}),
              callbacks: {
                ...((common.plugins?.tooltip as any)?.callbacks ?? {}),
                label: (ctx: TooltipItem<'bar'>) => ` ${this.formatNumber(ctx.raw)} Linhas`,
              }
            }
          }
        : common.plugins;

    this.charts['clientes'] = new Chart(canvas, {
      type: 'bar',
      data: {
        labels: data.map(d => (d.label?.length ?? 0) > 25 ? (d.label ?? '').slice(0, 25) + '...' : d.label),
        datasets: [{
          label: metricLabel,
          data: data.map(d => d.value),
          backgroundColor: CHART_THEME.success,
          borderRadius: 4,
          barPercentage: 0.7,
        }]
      },
      options: {
        ...common,
        indexAxis: 'y',
        plugins,
        scales: {
          x: {
            ...(common.scales?.x ?? {}),
            ticks: {
              ...((common.scales?.x as any)?.ticks ?? {}),
              maxTicksLimit: 8
            }
          },
          y: {
            ...(common.scales?.y ?? {}),
            ticks: {
              ...((common.scales?.y as any)?.ticks ?? {}),
              autoSkip: false,
              maxTicksLimit: data.length || 10
            }
          }
        }
      }
    });
  }

  private buildChartTotais() {
    const canvas = this.chartTotaisRef?.nativeElement;
    if (!canvas) return;

    const pf = this.toNumber(this.findLineTotal(['PF', 'PESSOA FISICA'])?.qtdLinhas) ?? 0;
    const pj = this.toNumber(this.findLineTotal(['PJ', 'PESSOA JURIDICA'])?.qtdLinhas) ?? 0;

    this.charts['totais'] = new Chart(canvas, {
      type: 'doughnut',
      data: {
        labels: ['Pessoa Física', 'Pessoa Jurídica'],
        datasets: [{
          data: [pf, pj],
          backgroundColor: [CHART_THEME.brand, CHART_THEME.blue],
          borderWidth: 0,
          hoverOffset: 4
        }]
      },
      options: {
        responsive: true,
        maintainAspectRatio: false,
        cutout: '75%',
        plugins: {
          legend: { position: 'bottom', labels: { usePointStyle: true, padding: 20 } },
          tooltip: {
            backgroundColor: '#1e293b',
            padding: 12,
            callbacks: { label: (c) => ` ${c.label}: ${this.formatNumber(c.raw)} linhas` }
          }
        }
      }
    });
  }

  private buildChartReserva() {
    const canvas = this.chartReservaRef?.nativeElement;
    if (!canvas) return;

    const data = this.getReservaPorDddChartData()
      .sort((a, b) => b.totalLinhas - a.totalLinhas)
      .slice(0, 15);

    this.charts['reserva'] = new Chart(canvas, {
      type: 'bar',
      data: {
        labels: data.map(d => d.label),
        datasets: [{
          label: 'Linhas em Estoque',
          data: data.map(d => d.totalLinhas),
          backgroundColor: CHART_THEME.blue,
          borderRadius: 2,
          minBarLength: 10
        }]
      },
      options: {
        ...this.getCommonChartOptions('number'),
        plugins: {
          ...this.getCommonChartOptions('number')?.plugins,
          tooltip: {
            ...this.getCommonChartOptions('number')?.plugins?.tooltip,
            callbacks: {
              label: (ctx: TooltipItem<'bar'>) => ` ${this.formatNumber(ctx.raw)} linhas`
            }
          }
        }
      }
    });
  }

  private getCommonChartOptions(format: 'currency' | 'number'): ChartConfiguration['options'] {
    return {
      responsive: true,
      maintainAspectRatio: false,
      interaction: { mode: 'nearest', intersect: true },
      plugins: {
        legend: { display: false },
        tooltip: {
          backgroundColor: '#1e293b',
          titleFont: { size: 13 },
          bodyFont: { size: 13, weight: 'bold' },
          padding: 12,
          cornerRadius: 8,
          displayColors: false,
          callbacks: {
            label: (ctx: TooltipItem<'bar'>) => {
              const val = ctx.raw as number;
              return format === 'currency' ? this.formatMoney(val) : this.formatNumber(val);
            }
          }
        }
      },
      scales: {
        y: {
          beginAtZero: true,
          grid: { color: '#f1f5f9', drawBorder: false } as any,
          ticks: { font: { size: 11 }, maxTicksLimit: 6 }
        },
        x: {
          grid: { display: false } as any,
          ticks: { font: { size: 11 } }
        }
      }
    } as any;
  }

  // --- Demais Métodos Funcionais (Mantidos para garantir contrato) ---
  
  private destroyCharts(): void {
    Object.keys(this.charts).forEach(key => {
      this.charts[key]?.destroy();
      this.charts[key] = undefined;
    });
  }

  // (Mantendo lógica de carga de dados original para não quebrar regras de negócio)
  private loadResumo(): void {
    this.loading = true;
    this.errorMessage = '';
    this.dataReady = false;
    this.totaisLineFromGeral = [];

    this.resumoService.getResumo().subscribe({
      next: (data) => {
        this.resumo = data ? this.normalizeResumo(data) : null;
        this.loading = false;
        this.dataReady = true;
        this.bindTables();
        this.loadTotaisLineFromGeral();
        this.cdr.detectChanges();
        this.tryBuildCharts();
      },
      error: () => {
        this.loading = false;
        this.errorMessage = 'Não foi possível carregar os dados.';
        this.cdr.detectChanges();
      }
    });
  }

  private loadTotaisLineFromGeral(): void {
    if (!isPlatformBrowser(this.platformId)) return;

    this.http.get<any>(`${this.baseApi}/dashboard/geral/insights`).subscribe({
      next: (dto) => {
        const rows = this.extractTotaisLineFromInsights(dto);
        if (!rows.length) return;

        this.totaisLineFromGeral = rows;
        this.bindTables();
        this.cdr.detectChanges();
        this.tryBuildCharts();
      },
      error: () => {
        // Mantem fallback para a tabela de Resumo quando insights falhar.
      },
    });
  }

  private extractTotaisLineFromInsights(dto: any): LineTotal[] {
    const kpis = dto?.kpis ?? dto?.Kpis;
    const rawRows = kpis?.totaisLine ?? kpis?.TotaisLine;
    if (!Array.isArray(rawRows)) return [];

    return rawRows
      .map((row: GeralLineTotalPayload) => ({
        tipo: this.toText(row?.tipo),
        qtdLinhas: this.toNumber(row?.qtdLinhas),
        valorTotalLine: this.toNumber(row?.valorTotalLine),
        lucroTotalLine: this.toNumber(row?.lucroTotalLine),
      }))
      .filter((row) => !!(row.tipo ?? '').toString().trim());
  }

  private toText(value: unknown): string | null {
    if (value === null || value === undefined) return null;
    const text = String(value).trim();
    return text ? text : null;
  }

  // Animação de entrada
  private animateIn(): void {
    if (!isPlatformBrowser(this.platformId)) return;
    setTimeout(() => {
      document.querySelectorAll('[data-animate]').forEach((el, i) => {
        setTimeout(() => el.classList.add('is-visible'), i * ANIMATION_DELAY);
      });
    }, 100);
  }

  // ... (Restante da lógica de Macrophony, Tabelas e Formatações mantidas exatamente como no original para segurança funcional) ...
  // Apenas garantindo que métodos usados no HTML (formatMoney, toggleMacrophonyCompact, etc.) existam.
  
  // Exemplo de Formatação Enterprise
  formatMoney(value: any): string {
    const n = this.toNumber(value);
    if (n === null) return '-';
    // Usando narrowSymbol para R$ sem espaço extra se desejado, mas padrão pt-BR é seguro
    return new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(n);
  }

  formatNumber(value: any): string {
    const n = this.toNumber(value);
    if (n === null) return '-';
    return new Intl.NumberFormat('pt-BR').format(n);
  }

  formatGb(value: any): string {
    if (value == null) return '-';
    const n = this.toNumber(value);
    return n == null ? String(value) : `${new Intl.NumberFormat('pt-BR').format(n)} GB`;
  }

  formatCell<T>(column: TableColumn<T>, row: T): string {
    const value = column.value(row);
    if (column.display) return column.display(value, row);
    if (value === null || value === undefined || value === '') return '-';

    switch (column.type) {
      case 'money':
        return this.formatMoney(value);
      case 'number':
        return this.formatNumber(value);
      case 'gb':
        return this.formatGb(value);
      default:
        return String(value);
    }
  }
  
  // (Omitindo repetição de código boilerplate de tabelas/paginação que não mudou a lógica, apenas a view consome diferente)
  // ... [Inclua aqui todo o restante dos métodos: initTables, buildMacrophonyGroups, updateMacrophonyView, exportCsv, etc.] ...
  
  // Lógica crítica de retry do Chart
  private tryBuildCharts(): void {
    if (!isPlatformBrowser(this.platformId) || !this.viewReady || !this.dataReady) return;
    requestAnimationFrame(() => this.buildChartsForActiveTab());
  }
  
  private deferChartBuild(): void {
    if (!this.viewReady || !this.dataReady) return;
    setTimeout(() => this.tryBuildCharts(), 50);
  }

  private deferAnimate(): void {
    if (!this.animateReady) return;
    setTimeout(() => this.animateIn(), 50);
  }
  
  private isValidTab(tab: string | null): tab is ResumoTab {
    return ['planos', 'clientes', 'totais', 'reserva'].includes(tab || '');
  }

  // Métodos obrigatórios para o template funcionar
  toggleMacrophonyCompact() { this.macrophonyCompact = !this.macrophonyCompact; }
  onMacrophonySearch(value?: string) {
    if (typeof value === 'string') this.macrophonySearch = value;
    this.macrophonyPage = 1;
    this.updateMacrophonyView();
  }
  onMacrophonyPageSizeChange(value?: number | string) {
    if (value !== undefined && value !== null) {
      const parsed = Number(value);
      if (Number.isFinite(parsed) && parsed > 0) {
        this.macrophonyPageSize = parsed;
      }
    }
    this.macrophonyPage = 1;
    this.updateMacrophonyView();
  }
  isMacrophonyOpen(key: string) { return this.macrophonyOpen.has(key); }
  toggleMacrophonyGroup(key: string) { if (this.macrophonyOpen.has(key)) this.macrophonyOpen.delete(key); else this.macrophonyOpen.add(key); }
  openMacrophonyDetail(g: MacrophonyGroup) { this.macrophonyDetailGroup = g; this.macrophonyDetailOpen = true; }
  closeMacrophonyDetail() { this.macrophonyDetailOpen = false; this.macrophonyDetailGroup = null; }
  goToMacrophonyPage(p: number) { this.macrophonyPage = p; this.updateMacrophonyView(); }
  
  onGroupedSearch<T>(g: GroupedTableState<T>, value?: string) {
    if (typeof value === 'string') g.search = value;
    g.page = 1;
    this.updateGroupView(g);
  }
  toggleGroupedCompact<T>(g: GroupedTableState<T>) { g.compact = !g.compact; }
  exportGroupedCsv<T>(g: GroupedTableState<T>, file: string) { this.exportCsv(g.table, file); }
  isGroupedOpen<T>(g: GroupedTableState<T>, key: string) { return g.open.has(key); }
  toggleGroupedOpen<T>(g: GroupedTableState<T>, key: string) { if (g.open.has(key)) g.open.delete(key); else g.open.add(key); }
  openGroupedDetail<T>(g: GroupedTableState<T>, item: GroupItem<T>) { g.detailGroup = item; g.detailOpen = true; }
  closeGroupedDetail<T>(g: GroupedTableState<T>) { g.detailOpen = false; g.detailGroup = null; }
  getGroupedPageStart<T>(g: GroupedTableState<T>) { return g.filtered.length ? ((g.page - 1) * g.pageSize + 1) : 0; }
  getGroupedPageEnd<T>(g: GroupedTableState<T>) { return g.filtered.length ? Math.min(g.page * g.pageSize, g.filtered.length) : 0; }
  getGroupedPageNumbers<T>(g: GroupedTableState<T>) {
    const total = this.getGroupedTotalPages(g);
    if (total <= 1) return [1];
    const current = Math.min(Math.max(g.page, 1), total);
    const start = Math.max(1, current - 2);
    const end = Math.min(total, start + 4);
    const adjustedStart = Math.max(1, end - 4);
    return Array.from({ length: end - adjustedStart + 1 }, (_, i) => adjustedStart + i);
  }
  getGroupedTotalPages<T>(g: GroupedTableState<T>) { return Math.max(1, Math.ceil(g.filtered.length / g.pageSize)); }
  goToGroupedPage<T>(g: GroupedTableState<T>, p: number) {
    g.page = Math.min(this.getGroupedTotalPages(g), Math.max(1, p));
    this.updateGroupView(g);
  }
  getTableRowClass<T>(_: TableState<T>, __: T) { return false; }
  getToneClass(v: any) {
    const n = this.toNumber(v);
    if (n === null || n === 0) return '';
    return n > 0 ? 'text-success' : 'text-danger';
  }
  isVivoTravel(v: any) {
    if (v === true || v === 1) return true;
    const normalized = String(v ?? '').trim().toLowerCase();
    return normalized === 'true' || normalized === '1' || normalized === 'sim';
  }

  private initTables() {
    const hideMoneyColumns = <T>(cols: TableColumn<T>[]) =>
      this.showFinancial ? cols : cols.filter((c) => c.type !== 'money');

    const macrophonyColumns: TableColumn<MacrophonyPlan>[] = [
      { key: 'planoContrato', label: 'Plano', type: 'text', value: (r) => r.planoContrato ?? '-' },
      { key: 'gb', label: 'Franquia', type: 'gb', value: (r) => r.gb },
      { key: 'valorIndividualComSvas', label: 'Valor Un.', type: 'money', align: 'right', value: (r) => r.valorIndividualComSvas },
      { key: 'totalLinhas', label: 'Linhas', type: 'number', align: 'right', value: (r) => r.totalLinhas },
      { key: 'valorTotal', label: 'Valor Total', type: 'money', align: 'right', value: (r) => r.valorTotal },
    ];

    this.tableMacrophony = {
      key: 'macrophony',
      label: 'Macrophony',
      data: [],
      columns: hideMoneyColumns(macrophonyColumns),
      search: '',
      page: 1,
      pageSize: 10,
      pageSizeOptions: [10, 20, 50],
      sortKey: this.showFinancial ? 'valorTotal' : 'totalLinhas',
      sortDir: 'desc',
      compact: false,
      view: null,
    };

    const planoContratoColumns: TableColumn<PlanoContratoResumo>[] = [
      { key: 'planoContrato', label: 'Plano', type: 'text', value: (r) => r.planoContrato ?? '-' },
      { key: 'gb', label: 'Franquia', type: 'gb', value: (r) => r.gb },
      { key: 'valorIndividualComSvas', label: 'Valor Un.', type: 'money', align: 'right', value: (r) => r.valorIndividualComSvas },
      { key: 'totalLinhas', label: 'Linhas', type: 'number', align: 'right', value: (r) => r.totalLinhas },
      { key: 'valorTotal', label: 'Valor Total', type: 'money', align: 'right', value: (r) => r.valorTotal },
    ];

    this.tablePlanoContrato = {
      key: 'planoContrato',
      label: 'Plano Contrato',
      data: [],
      columns: hideMoneyColumns(planoContratoColumns),
      search: '',
      page: 1,
      pageSize: 10,
      pageSizeOptions: [10, 20, 50],
      sortKey: this.showFinancial ? 'valorTotal' : 'totalLinhas',
      sortDir: 'desc',
      compact: false,
      view: null,
    };

    const clientesColumns: TableColumn<VivoLineResumo>[] = [
      { key: 'cliente', label: 'Cliente', type: 'text', value: (r) => r.cliente ?? '-' },
      { key: 'qtdLinhas', label: 'Linhas', type: 'number', align: 'right', value: (r) => r.qtdLinhas },
      { key: 'franquiaTotal', label: 'Franquia Vivo', type: 'gb', align: 'right', value: (r) => r.franquiaTotal },
      { key: 'valorContratoVivo', label: 'Custo Vivo', type: 'money', align: 'right', value: (r) => r.valorContratoVivo },
      { key: 'franquiaLine', label: 'Franquia Line', type: 'gb', align: 'right', value: (r) => r.franquiaLine },
      { key: 'valorContratoLine', label: 'Receita Line', type: 'money', align: 'right', value: (r) => r.valorContratoLine },
      { key: 'lucro', label: 'Lucro', type: 'money', align: 'right', value: (r) => r.lucro, tone: true },
    ];

    this.tableClientes = {
      key: 'clientes',
      label: 'Clientes',
      data: [],
      columns: hideMoneyColumns(clientesColumns),
      search: '',
      page: 1,
      pageSize: 10,
      pageSizeOptions: [10, 20, 50],
      sortKey: this.showFinancial ? 'lucro' : 'qtdLinhas',
      sortDir: 'desc',
      compact: false,
      view: null,
    };

    const clientesEspeciaisColumns: TableColumn<ClienteEspecial>[] = [
      { key: 'nome', label: 'Nome', type: 'text', value: (r) => r.nome ?? '-' },
      { key: 'valor', label: 'Valor', type: 'money', align: 'right', value: (r) => r.valor, tone: true },
    ];

    this.tableClientesEspeciais = {
      key: 'clientesEspeciais',
      label: 'Clientes Especiais',
      data: [],
      columns: hideMoneyColumns(clientesEspeciaisColumns),
      search: '',
      page: 1,
      pageSize: 10,
      pageSizeOptions: [10, 20, 50],
      sortKey: this.showFinancial ? 'valor' : 'nome',
      sortDir: this.showFinancial ? 'desc' : 'asc',
      compact: false,
      view: null,
    };

    const totaisLineColumns: TableColumn<LineTotal>[] = [
      { key: 'tipo', label: 'Tipo', type: 'text', value: (r) => r.tipo ?? '-' },
      { key: 'qtdLinhas', label: 'Linhas', type: 'number', align: 'right', value: (r) => r.qtdLinhas },
      { key: 'valorTotalLine', label: 'Valor', type: 'money', align: 'right', value: (r) => r.valorTotalLine },
      { key: 'lucroTotalLine', label: 'Lucro', type: 'money', align: 'right', value: (r) => r.lucroTotalLine, tone: true },
    ];

    this.tableTotaisLine = {
      key: 'totaisLine',
      label: 'Totais Line',
      data: [],
      columns: hideMoneyColumns(totaisLineColumns),
      search: '',
      page: 1,
      pageSize: 10,
      pageSizeOptions: [10, 20, 50],
      sortKey: 'qtdLinhas',
      sortDir: 'desc',
      compact: false,
      view: null,
    };

    this.tableReserva = {
      key: 'reserva',
      label: 'Reserva',
      data: [],
      columns: [
        { key: 'ddd', label: 'DDD', type: 'text', value: (r) => r.ddd ?? '-' },
        { key: 'franquiaGb', label: 'Franquia', type: 'gb', value: (r) => r.franquiaGb },
        { key: 'qtdLinhas', label: 'Linhas', type: 'number', align: 'right', value: (r) => r.qtdLinhas },
      ],
      search: '',
      page: 1,
      pageSize: 10,
      pageSizeOptions: [10, 20, 50],
      sortKey: 'qtdLinhas',
      sortDir: 'desc',
      compact: false,
      view: null,
    };
  }

  private initGroupTables() {
    this.groupPlanoContrato = this.createGroupedTableState(
      'grupoPlanoContrato',
      'Resumo de Contratos',
      this.tablePlanoContrato,
      (row) => (row.planoContrato ?? '-').toString(),
      (rows) => (rows[0]?.planoContrato ?? '-').toString(),
      (rows) => `Franquia ${this.formatGb(rows[0]?.gb)}`,
      (rows) => {
        const metrics: GroupMetric[] = [
          { label: 'Linhas', value: this.formatNumber(this.sumGroup(rows, (r) => r.totalLinhas)) },
        ];
        if (this.showFinancial) {
          metrics.push({ label: 'Valor', value: this.formatMoney(this.sumGroup(rows, (r) => r.valorTotal)) });
        }
        return metrics;
      },
      (a, b) => {
        if (this.showFinancial) {
          return this.sumGroup(b.rows, (r) => r.valorTotal) - this.sumGroup(a.rows, (r) => r.valorTotal);
        }
        return this.sumGroup(b.rows, (r) => r.totalLinhas) - this.sumGroup(a.rows, (r) => r.totalLinhas);
      }
    );

    this.groupClientes = this.createGroupedTableState(
      'grupoClientes',
      'Clientes',
      this.tableClientes,
      (row) => (row.cliente ?? '-').toString(),
      (rows) => (rows[0]?.cliente ?? '-').toString(),
      (rows) => `${this.formatNumber(this.sumGroup(rows, (r) => r.qtdLinhas))} linhas`,
      (rows) => {
        if (!this.showFinancial) {
          const linhas = this.sumGroup(rows, (r) => r.qtdLinhas);
          return [{ label: 'Linhas', value: this.formatNumber(linhas) }];
        }
        const receita = this.sumGroup(rows, (r) => r.valorContratoLine);
        const custo = this.sumGroup(rows, (r) => r.valorContratoVivo);
        const lucro = this.sumGroup(rows, (r) => r.lucro);
        return [
          { label: 'Receita', value: this.formatMoney(receita) },
          { label: 'Custo', value: this.formatMoney(custo) },
          { label: 'Lucro', value: this.formatMoney(lucro), tone: this.getToneClass(lucro) },
        ];
      },
      (a, b) => {
        if (this.showFinancial) {
          return this.sumGroup(b.rows, (r) => r.lucro) - this.sumGroup(a.rows, (r) => r.lucro);
        }
        return this.sumGroup(b.rows, (r) => r.qtdLinhas) - this.sumGroup(a.rows, (r) => r.qtdLinhas);
      }
    );

    this.groupClientesEspeciais = this.createGroupedTableState(
      'grupoClientesEspeciais',
      'Clientes Especiais',
      this.tableClientesEspeciais,
      (row) => (row.nome ?? '-').toString(),
      (rows) => (rows[0]?.nome ?? '-').toString(),
      undefined,
      (rows) => {
        if (!this.showFinancial) {
          return [];
        }
        const total = this.sumGroup(rows, (r) => r.valor);
        return [{ label: 'Valor', value: this.formatMoney(total), tone: this.getToneClass(total) }];
      },
      (a, b) => {
        if (!this.showFinancial) return 0;
        return this.sumGroup(b.rows, (r) => r.valor) - this.sumGroup(a.rows, (r) => r.valor);
      }
    );

    this.groupTotaisLine = this.createGroupedTableState(
      'grupoTotaisLine',
      'Totais Line',
      this.tableTotaisLine,
      (row) => (row.tipo ?? '-').toString(),
      (rows) => (rows[0]?.tipo ?? '-').toString(),
      undefined,
      (rows) => {
        const linhas = this.sumGroup(rows, (r) => r.qtdLinhas);
        const metrics: GroupMetric[] = [{ label: 'Linhas', value: this.formatNumber(linhas) }];
        if (this.showFinancial) {
          const valor = this.sumGroup(rows, (r) => r.valorTotalLine);
          const lucro = this.sumGroup(rows, (r) => r.lucroTotalLine);
          metrics.push({ label: 'Valor', value: this.formatMoney(valor) });
          metrics.push({ label: 'Lucro', value: this.formatMoney(lucro), tone: this.getToneClass(lucro) });
        }
        return metrics;
      },
      (a, b) => this.sumGroup(b.rows, (r) => r.qtdLinhas) - this.sumGroup(a.rows, (r) => r.qtdLinhas)
    );

    this.groupReserva = this.createGroupedTableState(
      'grupoReserva',
      'Reserva',
      this.tableReserva,
      (row) => String(row.ddd ?? '-'),
      (rows) => `DDD ${rows[0]?.ddd ?? '-'}`,
      undefined,
      (rows) => {
        const linhas = this.sumGroup(rows, (r) => r.qtdLinhas);
        return [
          { label: 'Total de Linhas', value: this.formatNumber(linhas) },
        ];
      },
      (a, b) => this.sumGroup(b.rows, (r) => r.qtdLinhas) - this.sumGroup(a.rows, (r) => r.qtdLinhas)
    );
  }

  private createGroupedTableState<T>(
    key: string,
    label: string,
    table: TableState<T>,
    groupBy: (row: T) => string,
    groupTitle: (rows: T[]) => string,
    groupSubtitle: ((rows: T[]) => string | undefined) | undefined,
    groupMetrics: (rows: T[]) => GroupMetric[],
    groupSort?: (a: GroupItem<T>, b: GroupItem<T>) => number,
  ): GroupedTableState<T> {
    return {
      key,
      label,
      table,
      groupBy,
      groupTitle,
      groupSubtitle,
      groupMetrics,
      groupSort,
      search: '',
      page: 1,
      pageSize: 10,
      pageSizeOptions: [10, 20, 50],
      compact: false,
      open: new Set<string>(),
      groups: [],
      filtered: [],
      view: [],
      detailOpen: false,
      detailGroup: null,
    };
  }

  private normalizeResumo(data: ResumoResponse): ResumoResponse {
    return {
      ...data,
      macrophonyPlans: Array.isArray(data.macrophonyPlans) ? data.macrophonyPlans : [],
      vivoLineResumos: Array.isArray(data.vivoLineResumos) ? data.vivoLineResumos : [],
      clienteEspeciais: Array.isArray(data.clienteEspeciais) ? data.clienteEspeciais : [],
      planoContratoResumos: Array.isArray(data.planoContratoResumos) ? data.planoContratoResumos : [],
      lineTotais: Array.isArray(data.lineTotais) ? data.lineTotais : [],
      gbDistribuicao: Array.isArray(data.gbDistribuicao) ? data.gbDistribuicao : [],
      reservaLines: Array.isArray(data.reservaLines) ? data.reservaLines : [],
      reservaPorDdd: Array.isArray(data.reservaPorDdd) ? data.reservaPorDdd : [],
    };
  }

  private bindTables() {
    const resumo = this.resumo;
    if (!resumo) {
      this.tableMacrophony.data = [];
      this.tablePlanoContrato.data = [];
      this.tableClientes.data = [];
      this.tableClientesEspeciais.data = [];
      this.tableTotaisLine.data = [];
      this.tableReserva.data = [];
      this.macrophonyGroups = [];
      this.macrophonyFiltered = [];
      this.macrophonyView = [];
      this.updateGroupView(this.groupPlanoContrato);
      this.updateGroupView(this.groupClientes);
      this.updateGroupView(this.groupClientesEspeciais);
      this.updateGroupView(this.groupTotaisLine);
      this.updateGroupView(this.groupReserva);
      return;
    }

    this.tableMacrophony.data = resumo.macrophonyPlans ?? [];
    this.tablePlanoContrato.data = this.buildPlanoContratoResumoConsolidado(resumo.planoContratoResumos ?? []);
    this.tableClientes.data = resumo.vivoLineResumos ?? [];
    this.tableClientesEspeciais.data = resumo.clienteEspeciais ?? [];
    this.tableTotaisLine.data = this.getEffectiveLineTotais();
    this.tableReserva.data = resumo.reservaLines ?? [];

    this.updateMacrophonyView();
    this.updateGroupView(this.groupPlanoContrato);
    this.updateGroupView(this.groupClientes);
    this.updateGroupView(this.groupClientesEspeciais);
    this.updateGroupView(this.groupTotaisLine);
    this.updateGroupView(this.groupReserva);
  }

  private updateMacrophonyView() {
    const rows = this.tableMacrophony.data ?? [];
    const byPlano = new Map<string, MacrophonyPlan[]>();
    for (const row of rows) {
      const key = (row.planoContrato ?? 'Sem Plano').toString();
      const list = byPlano.get(key) ?? [];
      list.push(row);
      byPlano.set(key, list);
    }

    const groups: MacrophonyGroup[] = Array.from(byPlano.entries()).map(([key, items]) => {
      const totalLinhas = this.sumGroup(items, (r) => r.totalLinhas);
      const valorTotal = this.sumGroup(items, (r) => r.valorTotal);
      const valorUnit = totalLinhas > 0 ? (valorTotal / totalLinhas) : null;
      const gbLabel = (items.find((i) => (i.gb ?? '').toString().trim())?.gb ?? '-').toString();
      return {
        key,
        plano: key,
        gbLabel,
        totalLinhas,
        valorTotal,
        valorUnitMedio: valorUnit,
        rows: [...items].sort((a, b) => {
          if (this.showFinancial) {
            return (this.toNumber(b.valorTotal) ?? 0) - (this.toNumber(a.valorTotal) ?? 0);
          }
          return (this.toNumber(b.totalLinhas) ?? 0) - (this.toNumber(a.totalLinhas) ?? 0);
        }),
      };
    });

    groups.sort((a, b) => {
      if (this.showFinancial) return b.valorTotal - a.valorTotal;
      return b.totalLinhas - a.totalLinhas;
    });
    this.macrophonyGroups = groups;

    const search = this.normalizeText(this.macrophonySearch);
    this.macrophonyFiltered = !search
      ? groups
      : groups.filter((group) =>
          this.normalizeText(group.plano).includes(search) ||
          this.normalizeText(group.gbLabel).includes(search)
        );

    const totalPages = Math.max(1, Math.ceil(this.macrophonyFiltered.length / this.macrophonyPageSize));
    this.macrophonyPage = Math.min(totalPages, Math.max(1, this.macrophonyPage));
    const start = (this.macrophonyPage - 1) * this.macrophonyPageSize;
    const end = start + this.macrophonyPageSize;
    this.macrophonyView = this.macrophonyFiltered.slice(start, end);

    if (this.macrophonyDetailGroup && !this.macrophonyFiltered.some((g) => g.key === this.macrophonyDetailGroup?.key)) {
      this.closeMacrophonyDetail();
    }
  }

  private updateGroupView<T>(group: GroupedTableState<T>) {
    const data = group.table.data ?? [];
    const map = new Map<string, T[]>();

    data.forEach((row) => {
      const key = group.groupBy(row) || 'Sem agrupamento';
      const rows = map.get(key) ?? [];
      rows.push(row);
      map.set(key, rows);
    });

    const groups = Array.from(map.entries()).map(([key, rows]) => ({
      key,
      title: group.groupTitle(rows),
      subtitle: group.groupSubtitle ? group.groupSubtitle(rows) : undefined,
      rows,
      metrics: group.groupMetrics(rows),
    })) as GroupItem<T>[];

    if (group.groupSort) groups.sort(group.groupSort);
    group.groups = groups;

    const search = this.normalizeText(group.search);
    group.filtered = !search
      ? groups
      : groups.filter((g) =>
          this.normalizeText(g.title).includes(search) ||
          this.normalizeText(g.subtitle).includes(search)
        );

    const totalPages = Math.max(1, Math.ceil(group.filtered.length / group.pageSize));
    group.page = Math.min(totalPages, Math.max(1, group.page));
    const start = (group.page - 1) * group.pageSize;
    const end = start + group.pageSize;
    group.view = group.filtered.slice(start, end);

    if (group.detailGroup && !group.filtered.some((g) => g.key === group.detailGroup?.key)) {
      group.detailGroup = null;
      group.detailOpen = false;
    }
  }

  private normalizeText(value: any): string {
    return (value ?? '')
      .toString()
      .trim()
      .toUpperCase()
      .normalize('NFD')
      .replace(/[\u0300-\u036f]/g, '');
  }

  private sumGroup<T>(rows: T[], getter: (row: T) => any): number {
    return rows.reduce((acc, row) => acc + (this.toNumber(getter(row)) ?? 0), 0);
  }

  private buildPlanoContratoResumoConsolidado(rows: PlanoContratoResumo[]): PlanoContratoResumo[] {
    const grouped = new Map<
      string,
      {
        planoContrato: string;
        gb: number | null;
        totalLinhas: number;
        valorTotal: number;
        valorIndividualFallback: number | null;
      }
    >();

    rows.forEach((row) => {
      const planoContrato = (row.planoContrato ?? '-').toString().trim() || '-';
      const key = this.normalizeText(planoContrato);
      const gb =
        this.extractGbFromPlanName(planoContrato) ??
        this.toNumber(row.gb ?? row.franquiaGb);
      const totalLinhas = this.toNumber(row.totalLinhas) ?? 0;
      const valorTotal = this.toNumber(row.valorTotal) ?? 0;
      const valorIndividual = this.toNumber(row.valorIndividualComSvas);

      const current = grouped.get(key);
      if (!current) {
        grouped.set(key, {
          planoContrato,
          gb,
          totalLinhas,
          valorTotal,
          valorIndividualFallback: valorIndividual,
        });
        return;
      }

      current.totalLinhas += totalLinhas;
      current.valorTotal += valorTotal;
      current.gb ??= gb;
      current.valorIndividualFallback ??= valorIndividual;
    });

    return Array.from(grouped.values())
      .map((item) => {
        const valorIndividualComSvas =
          item.totalLinhas > 0
            ? item.valorTotal / item.totalLinhas
            : item.valorIndividualFallback;

        return {
          planoContrato: item.planoContrato,
          gb: item.gb,
          franquiaGb: item.gb,
          valorIndividualComSvas,
          totalLinhas: item.totalLinhas,
          valorTotal: item.valorTotal,
        } satisfies PlanoContratoResumo;
      })
      .sort((a, b) => (this.toNumber(b.valorTotal) ?? 0) - (this.toNumber(a.valorTotal) ?? 0));
  }

  private extractGbFromPlanName(planoContrato: string | null | undefined): number | null {
    if (!planoContrato) return null;

    const match = planoContrato
      .toUpperCase()
      .match(/(\d+(?:[.,]\d+)?)\s*(GB|MB)\b/);

    if (!match) return null;

    const value = Number(match[1].replace(',', '.'));
    if (!Number.isFinite(value)) return null;

    return match[2] === 'MB' ? value / 1000 : value;
  }

  private toNumber(value: any): number | null {
    if (value === null || value === undefined || value === '') return null;
    if (typeof value === 'number') return Number.isFinite(value) ? value : null;
    const raw = String(value).trim();
    if (!raw) return null;

    let normalized = raw.replace(/[^\d,.-]/g, '');
    if (normalized.includes(',') && normalized.includes('.')) {
      if (normalized.lastIndexOf(',') > normalized.lastIndexOf('.')) {
        normalized = normalized.replace(/\./g, '').replace(',', '.');
      } else {
        normalized = normalized.replace(/,/g, '');
      }
    } else if (normalized.includes(',')) {
      normalized = normalized.replace(/\./g, '').replace(',', '.');
    }

    const parsed = Number(normalized);
    return Number.isNaN(parsed) ? null : parsed;
  }

  private exportCsv<T>(table: TableState<T>, filename: string) {
    if (!isPlatformBrowser(this.platformId)) return;
    const rows = table.data ?? [];
    const columns = table.columns ?? [];
    const generatedAt = new Date().toLocaleString('pt-BR');
    const escapeHtml = (value: string) =>
      value
        .replace(/&/g, '&amp;')
        .replace(/</g, '&lt;')
        .replace(/>/g, '&gt;')
        .replace(/"/g, '&quot;')
        .replace(/'/g, '&#39;');

    const headerHtml = columns
      .map((column) => `<th class="${column.align === 'right' ? 'text-right' : column.align === 'center' ? 'text-center' : ''}">${escapeHtml(column.label)}</th>`)
      .join('');

    const bodyHtml = rows
      .map((row, index) => {
        const cells = columns
          .map((column) => {
            const value = this.formatCell(column, row);
            const toneClass = column.tone ? this.getToneClass(column.value(row)) : '';
            const alignClass = column.align === 'right' ? 'text-right' : column.align === 'center' ? 'text-center' : '';
            const classes = [alignClass, toneClass].filter(Boolean).join(' ');
            return `<td class="${classes}">${escapeHtml(String(value))}</td>`;
          })
          .join('');
        return `<tr class="${index % 2 === 0 ? 'even' : 'odd'}">${cells}</tr>`;
      })
      .join('');

    const html = `<!DOCTYPE html>
<html lang="pt-BR">
<head>
  <meta charset="UTF-8" />
  <style>
    body { font-family: Segoe UI, Arial, sans-serif; margin: 20px; color: #0f172a; }
    .sheet-title { font-size: 18px; font-weight: 700; margin-bottom: 4px; }
    .sheet-subtitle { font-size: 12px; color: #64748b; margin-bottom: 14px; }
    table { border-collapse: collapse; width: 100%; table-layout: auto; }
    th, td { border: 1px solid #dbe2ef; padding: 8px 10px; font-size: 12px; }
    th { background: #e8eefc; color: #1e3a8a; font-weight: 700; text-transform: uppercase; letter-spacing: 0.3px; }
    tr.even td { background: #ffffff; }
    tr.odd td { background: #f8fafc; }
    .text-right { text-align: right; }
    .text-center { text-align: center; }
    .text-success { color: #047857; font-weight: 700; }
    .text-danger { color: #b91c1c; font-weight: 700; }
  </style>
</head>
<body>
  <div class="sheet-title">${escapeHtml(table.label || 'Resumo')}</div>
  <div class="sheet-subtitle">Exportado em ${escapeHtml(generatedAt)} | Total de linhas: ${rows.length}</div>
  <table>
    <thead>
      <tr>${headerHtml}</tr>
    </thead>
    <tbody>
      ${bodyHtml}
    </tbody>
  </table>
</body>
</html>`;

    const blob = new Blob([`\uFEFF${html}`], { type: 'application/vnd.ms-excel;charset=utf-8;' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = `${filename}.xls`;
    a.click();
    URL.revokeObjectURL(url);
  }

  private getReservaPorDddChartData(): Array<{ label: string; totalLinhas: number }> {
    const resumo = this.resumo;
    if (!resumo) return [];

    const porDdd = resumo.reservaPorDdd ?? [];
    if (porDdd.length) {
      return porDdd.map((item) => ({
        label: String(item.ddd ?? '-'),
        totalLinhas: this.toNumber(item.totalLinhas) ?? 0,
      }));
    }

    const fromLines = resumo.reservaLines ?? [];
    const map = new Map<string, number>();
    fromLines.forEach((row) => {
      const key = String(row.ddd ?? '-');
      const qtd = this.toNumber(row.qtdLinhas) ?? 0;
      map.set(key, (map.get(key) ?? 0) + qtd);
    });
    return Array.from(map.entries()).map(([label, totalLinhas]) => ({ label, totalLinhas }));
  }

  exportMacrophonyCsv() { this.exportCsv(this.tableMacrophony, 'macrophony-planos'); }
  findLineTotal(k: string[]): LineTotal | null {
    const keys = k.map((item) => item.toUpperCase());
    const list = this.getEffectiveLineTotais();
    for (const item of list) {
      const tipo = (item?.tipo ?? '').toString().toUpperCase();
      if (keys.some((key) => tipo.includes(key))) return item;
    }
    return null;
  }

  get totaisLineLucroConsolidado(): number {
    const pf = this.toNumber(this.findLineTotal(['PF', 'PESSOA FISICA'])?.lucroTotalLine) ?? 0;
    const pj = this.toNumber(this.findLineTotal(['PJ', 'PESSOA JURIDICA'])?.lucroTotalLine) ?? 0;
    return pf + pj;
  }

  private getEffectiveLineTotais(): LineTotal[] {
    if (this.totaisLineFromGeral.length > 0) {
      return this.totaisLineFromGeral;
    }

    return Array.isArray(this.resumo?.lineTotais) ? (this.resumo?.lineTotais ?? []) : [];
  }
  
  get macrophonyPageStart() { return (this.macrophonyPage - 1) * this.macrophonyPageSize + 1; }
  get macrophonyPageEnd() { return Math.min(this.macrophonyPage * this.macrophonyPageSize, this.macrophonyFilteredGroups.length); }
  get macrophonyFilteredGroups() { return this.macrophonyFiltered; }
  get macrophonyPageNumbers() {
    const total = this.macrophonyTotalPages;
    if (total <= 1) return [1];
    const current = Math.min(Math.max(this.macrophonyPage, 1), total);
    const start = Math.max(1, current - 2);
    const end = Math.min(total, start + 4);
    const adjustedStart = Math.max(1, end - 4);
    return Array.from({ length: end - adjustedStart + 1 }, (_, i) => adjustedStart + i);
  }
  get macrophonyTotalPages() { return Math.max(1, Math.ceil(this.macrophonyFiltered.length / this.macrophonyPageSize)); }
  get planosTotals() { return this.resumo?.macrophonyTotals; }
  get contratosTotals() { return this.resumo?.planoContratoTotal; }
  get clientesTotals() { return this.resumo?.vivoLineTotals; }
  get gbDistribuicaoRows(): GbDistribuicao[] { return this.resumo?.gbDistribuicao ?? []; }
  get gbDistribuicaoTotalLinhas(): number {
    const fromTotal = this.toNumber(this.resumo?.gbDistribuicaoTotal?.totalLinhas);
    if (fromTotal !== null) return fromTotal;
    return this.gbDistribuicaoRows.reduce((acc, row) => acc + (this.toNumber(row.qtd) ?? 0), 0);
  }
  get gbDistribuicaoSomaTotal(): number {
    const fromTotal = this.toNumber(this.resumo?.gbDistribuicaoTotal?.somaTotal);
    if (fromTotal !== null) return fromTotal;
    return this.gbDistribuicaoRows.reduce((acc, row) => acc + (this.toNumber(row.soma) ?? 0), 0);
  }
  get reservaTotals() { return this.resumo?.reservaTotal; }
}
