import {
  Component,
  ElementRef,
  ViewChild,
  Inject,
  PLATFORM_ID,
  OnInit,
  AfterViewInit,
  ChangeDetectorRef,
  OnDestroy,
  HostListener
} from '@angular/core';
import { isPlatformBrowser, CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import {
  HttpClient,
  HttpParams,
  HttpErrorResponse
} from '@angular/common/http';
import { NavigationEnd, Router } from '@angular/router';
import { CustomSelectComponent } from '../../components/custom-select/custom-select';
import { PlanAutoFillService } from '../../services/plan-autofill.service';
import { AuthService } from '../../services/auth.service';
import { firstValueFrom, Subscription, filter } from 'rxjs';
import { environment } from '../../../environments/environment';
import { confirmDeletionWithTyping } from '../../utils/destructive-confirmation';
import {
  BATCH_MASS_COLUMN_GUIDE,
  type BatchMassApplyMode,
  buildBatchMassExampleText,
  buildBatchMassHeaderLine,
  type BatchMassPreviewResult,
  type BatchMassSeparatorMode,
  buildBatchMassPreview,
  mergeMassRows
} from './batch-mass-input.util';

type SortDir = 'asc' | 'desc';
type CreateMode = 'NEW_CLIENT' | 'NEW_LINE_IN_GROUP';
type CreateEntryMode = 'SINGLE' | 'BATCH';
type AdditionalMode = 'ALL' | 'WITH' | 'WITHOUT';
type AdditionalServiceKey = 'gvd' | 'skeelo' | 'news' | 'travel' | 'sync' | 'dispositivo';

interface LineRow {
  id: string;
  item: string;
  linha: string;
  chip?: string;
  cliente: string;
  usuario: string;
  status: string;
  skil: string;
  contrato: string;
}

interface ApiPagedResult<T> {
  page: number;
  pageSize: number;
  total: number;
  items: T[];
}

interface ApiLineList {
  id: string;
  item: number;
  linha: string | null;
  chip?: string | null;
  cliente: string | null;
  usuario: string | null;
  vencConta: string | null;
  status?: string | null;
  skil?: string | null;
  gestaoVozDados?: number | null;
  skeelo?: number | null;
  vivoNewsPlus?: number | null;
  vivoTravelMundo?: number | null;
  vivoSync?: number | null;
  vivoGestaoDispositivo?: number | null;
}

interface ApiLineDetail {
  id: string;
  item: number;
  qtdLinhas?: number | null;
  conta?: string | null;
  linha?: string | null;
  chip?: string | null;
  tipoDeChip?: string | null;
  cliente?: string | null;
  usuario?: string | null;
  planoContrato?: string | null;
  status?: string | null;
  skil?: string | null;
  modalidade?: string | null;
  dataBloqueio?: string | null;
  cedente?: string | null;
  solicitante?: string | null;
  dataEntregaOpera?: string | null;
  dataEntregaCliente?: string | null;
  dtEfetivacaoServico?: string | null;
  dtTerminoFidelizacao?: string | null;
  vencConta?: string | null;

  franquiaVivo?: number | null;
  valorPlanoVivo?: number | null;
  gestaoVozDados?: number | null;
  skeelo?: number | null;
  vivoNewsPlus?: number | null;
  vivoTravelMundo?: number | null;
  vivoGestaoDispositivo?: number | null;
  vivoSync?: number | null;
  valorContratoVivo?: number | null;

  franquiaLine?: number | null;
  franquiaGestao?: number | null;
  locacaoAp?: number | null;
  valorContratoLine?: number | null;

  desconto?: number | null;
  lucro?: number | null;
}

type UpdateMobileLineRequest = Omit<ApiLineDetail, 'id'>;
type CreateMobileLineRequest = Omit<ApiLineDetail, 'id'>;

interface ClientGroupDto {
  cliente: string;
  totalLinhas: number;
  ativos: number;
  bloqueados: number;
}

interface AccountCompanyOption {
  empresa: string;
  contas: string[];
}

interface ReservaLineOption {
  value: string;
  label: string;
  linha: string;
  chip?: string;
  usuario?: string;
}

interface CreateBatchLineDraft extends Partial<CreateMobileLineRequest> {
  uid: number;
  linha: string;
  chip: string;
  tipoDeChip: string;
  usuario: string;
  contaEmpresa?: string;
}

interface BatchLineValidation {
  uid: number;
  index: number;
  linhaDigits: string;
  errors: string[];
}

interface BatchValidationSummary {
  total: number;
  valid: number;
  invalid: number;
  duplicates: number;
}

interface CreateMobileLinesBatchRequest {
  lines: CreateMobileLineRequest[];
}

interface CreateMobileLinesBatchResponse {
  created?: number;
  items?: Array<{
    id: string;
    item: number;
    linha?: string | null;
    cliente?: string | null;
  }>;
}

interface BatchExcelIssueDto {
  column?: string | null;
  message: string;
}

interface BatchExcelPreviewRowDto {
  sourceRowNumber: number;
  sourceItem?: number | null;
  generatedItemPreview?: number | null;
  valid: boolean;
  duplicateLinhaInFile?: boolean;
  duplicateChipInFile?: boolean;
  duplicateLinhaInSystem?: boolean;
  duplicateChipInSystem?: boolean;
  data: Partial<CreateMobileLineRequest>;
  errors: BatchExcelIssueDto[];
  warnings: BatchExcelIssueDto[];
}

interface BatchExcelPreviewResultDto {
  fileName?: string | null;
  sheetName?: string | null;
  nextItemStart: number;
  totalRows: number;
  validRows: number;
  invalidRows: number;
  duplicateRows: number;
  canProceed: boolean;
  headerErrors: BatchExcelIssueDto[];
  headerWarnings: BatchExcelIssueDto[];
  rows: BatchExcelPreviewRowDto[];
}

interface AssignReservaLinesRequestDto {
  clienteDestino: string;
  usuarioDestino?: string | null;
  skilDestino?: string | null;
  lineIds: string[];
}

interface MoveLinesToReservaRequestDto {
  lineIds: string[];
}

interface AssignReservaLineItemResultDto {
  id: string;
  item?: number;
  linha?: string | null;
  chip?: string | null;
  clienteAnterior?: string | null;
  clienteNovo?: string | null;
  success: boolean;
  message: string;
}

interface AssignReservaLinesResultDto {
  requested: number;
  updated: number;
  failed: number;
  items: AssignReservaLineItemResultDto[];
}


@Component({
  standalone: true,
  imports: [CommonModule, FormsModule, CustomSelectComponent],
  templateUrl: './geral.html',
  styleUrls: ['./geral.scss']
})
export class Geral implements OnInit, AfterViewInit, OnDestroy {
  readonly batchMassColumnGuide = BATCH_MASS_COLUMN_GUIDE;
  toastMessage = '';

  @ViewChild('successToast', { static: false }) successToast!: ElementRef;
  @ViewChild('excelInput') excelInput!: ElementRef<HTMLInputElement>;
  @ViewChild('batchExcelInput') batchExcelInput?: ElementRef<HTMLInputElement>;

  @ViewChild('editModal', { static: false }) editModal!: ElementRef<HTMLElement>;
  @ViewChild('createModal', { static: false }) createModal!: ElementRef<HTMLElement>;
  @ViewChild('detailModal', { static: false }) detailModal!: ElementRef<HTMLElement>;
  @ViewChild('financeModal', { static: false }) financeModal!: ElementRef<HTMLElement>;

  constructor(
    @Inject(PLATFORM_ID) private platformId: object,
    private http: HttpClient,
    private cdr: ChangeDetectorRef,
    private planAutoFill: PlanAutoFillService,
    private authService: AuthService,
    private router: Router
  ) {}

  private readonly apiBase = (() => {
    const raw = (environment.apiUrl || '').replace(/\/+$/, '');
    const apiBase = raw.toLowerCase().endsWith('/api') ? raw : `${raw}/api`;
    return `${apiBase}/lines`;
  })();
  private readonly templatesApiBase = (() => {
    const raw = (environment.apiUrl || '').replace(/\/+$/, '');
    const apiBase = raw.toLowerCase().endsWith('/api') ? raw : `${raw}/api`;
    return `${apiBase}/templates`;
  })();
  loading = false;
  isSysAdmin = false;
  isGestor = false;
  isClientRestricted = false;

  rows: LineRow[] = [];
  clientGroups: ClientGroupDto[] = [];
  groupLines: LineRow[] = [];
  expandedGroup: string | null = null;
  loadingLines = false;

  searchTerm = '';
  filterSkil: 'ALL' | 'PF' | 'PJ' | 'RESERVA' = 'ALL';
  additionalMode: AdditionalMode = 'ALL';
  selectedAdditionalServices: AdditionalServiceKey[] = [];
  readonly additionalServiceOptions: Array<{ key: AdditionalServiceKey; label: string }> = [
    { key: 'gvd', label: 'Gestão Voz e Dados' },
    { key: 'skeelo', label: 'Skeelo' },
    { key: 'news', label: 'Vivo News Plus' },
    { key: 'travel', label: 'Vivo Travel Mundo' },
    { key: 'sync', label: 'Vivo Sync' },
    { key: 'dispositivo', label: 'Vivo Gestão Dispositivo' }
  ];

  clientsList: string[] = [];
  loadingClientsList = false;

  selectedClients: string[] = [];
  showClientMenu = false;
  showAdditionalMenu = false;
  clientSearchTerm = '';

  viewMode: 'GROUPS' | 'TABLE' = 'GROUPS';

  sortKey: keyof LineRow = 'item';
  sortDir: SortDir = 'asc';
  page = 1;
  pageSize = 10;
  pageSizeOptions = [10, 20, 50, 100];
  total = 0;

  detailOpen = false;
  financeOpen = false;
  editOpen = false;
  editSaving = false;

  createOpen = false;
  createSaving = false;
  createMode: CreateMode = 'NEW_CLIENT';
  createEntryMode: CreateEntryMode = 'SINGLE';
  createBatchLines: CreateBatchLineDraft[] = [];
  selectedBatchLineUid: number | null = null;
  batchDetailOpen = false;
  batchMassInputText = '';
  batchMassSeparatorMode: BatchMassSeparatorMode = 'AUTO';
  batchMassPreview: BatchMassPreviewResult | null = null;
  batchExcelPreview: BatchExcelPreviewResultDto | null = null;
  batchExcelPreviewLoading = false;
  batchExcelTemplateDownloading = false;
  batchExcelPreviewApplyMode: BatchMassApplyMode = 'ADD';
  createBatchValidationByUid: Record<number, BatchLineValidation> = {};
  createBatchValidationSummary: BatchValidationSummary = { total: 0, valid: 0, invalid: 0, duplicates: 0 };

  reservaSelectedLineIds: string[] = [];
  reservaTransferOpen = false;
  reservaTransferSaving = false;
  moveToReservaOpen = false;
  moveToReservaSaving = false;
  reservaTransferClients: string[] = [];
  reservaTransferModel: { clienteDestino: string; usuarioDestino: string; skilDestino: string } = {
    clienteDestino: '',
    usuarioDestino: '',
    skilDestino: ''
  };
  reservaTransferLastResult: AssignReservaLinesResultDto | null = null;
  moveToReservaLastResult: AssignReservaLinesResultDto | null = null;

  detailData: any = null;
  financeData: any = null;
  editModel: any = null;

  private editingId: string | null = null;
  private searchTimer: any = null;
  private navigationSub?: Subscription;
  private keepPageOnNextGroupsLoad = false;

  private searchResolvedClient: string | null = null;
  private kpiRequestVersion = 0;
  private groupsRequestVersion = 0;
  private linesRequestVersion = 0;
  private clientsRequestVersion = 0;
  private createBatchUidSeed = 0;

  loadingKpis = false;
  kpiTotalClientes = 0;
  kpiTotalLinhas = 0;
  kpiAtivas = 0;
  kpiBloqueadas = 0;

  readonly statusOptions = ['ATIVO', 'BLOQUEIO PERDA/ROUBO', 'BLOQUEIO 120 DIAS'];
  readonly skilOptions = ['PESSOA FÍSICA', 'PESSOA JURÍDICA', 'RESERVA'];

  planOptions = [
    'SMART EMPRESAS 0.2GB TE',
    'SMART EMPRESAS 0.5GB TE',
    'SMART EMPRESAS 2GB D',
    'SMART EMPRESAS 4GB',
    'SMART EMPRESAS 6GB',
    'SMART EMPRESAS 8GB',
    'SMART EMPRESAS 10GB',
    'SMART EMPRESAS 20GB',
    'SMART EMPRESAS 50GB',
    'M2M 20MB',
    'M2M 50MB'
  ];

  private readonly fallbackAccountCompanies: AccountCompanyOption[] = [
    { empresa: 'CLARO LINE MÓVEL', contas: ['172593311', '172593840'] },
    { empresa: 'VIVO MACROPHONY', contas: ['0430237019', '0437488125', '0449508564', '0454371844'] },
    { empresa: 'VIVO LINE MÓVEL', contas: ['0435288088'] },
    { empresa: 'TIM LINE MÓVEL', contas: ['0072046192'] }
  ];

  accountCompanies: AccountCompanyOption[] = [...this.fallbackAccountCompanies];
  loadingAccountCompanies = false;
  createReservaLineOptions: ReservaLineOption[] = [];
  loadingCreateReservaLines = false;
  private createReservaLineLookup = new Map<string, ReservaLineOption>();

  get contaEmpresaOptions(): string[] {
    return this.accountCompanies.map((x) => x.empresa);
  }

  get contaOptionsForCreate(): string[] {
    return this.getContasByEmpresa(this.createModel?.contaEmpresa);
  }

  get contaEmpresaOptionsForEdit(): string[] {
    return this.mergeOption(this.editModel?.contaEmpresa, this.contaEmpresaOptions);
  }

  get contaOptionsForEdit(): string[] {
    const empresaSelecionada = (this.editModel?.contaEmpresa ?? '').toString().trim();
    const baseOptions = empresaSelecionada
      ? this.getContasByEmpresa(empresaSelecionada)
      : this.getAllContas();

    return this.mergeOption(this.editModel?.conta, baseOptions);
  }

  get planOptionsForEdit(): string[] {
    return this.mergeOption(this.editModel?.planoContrato, this.planOptions);
  }

  get statusOptionsForEdit(): string[] {
    return this.mergeOption(this.editModel?.status, this.statusOptions);
  }

  get skilOptionsForEdit(): string[] {
    return this.mergeOption(this.editModel?.skil, this.skilOptions);
  }

  createModel: any = {
    cliente: '',
    docType: 'PF',
    docNumber: '',
    contaEmpresa: '',
    reservaLineId: '',
    linha: '',
    chip: '',
    tipoDeChip: '',
    usuario: '',
    status: '',
    planoContrato: '',
    conta: '',
    vencConta: '',
    skil: 'PESSOA FÍSICA',
    modalidade: '',
    item: 0,
    cedente: '',
    solicitante: '',
    dataBloqueio: '',
    dataEntregaOpera: '',
    dataEntregaCliente: '',
    dtEfetivacaoServico: '',
    dtTerminoFidelizacao: '',
    franquiaVivo: null,
    valorPlanoVivo: null,
    gestaoVozDados: null,
    skeelo: null,
    vivoNewsPlus: null,
    vivoTravelMundo: null,
    vivoGestaoDispositivo: null,
    vivoSync: null,
    valorContratoVivo: null,
    franquiaLine: null,
    franquiaGestao: null,
    locacaoAp: null,
    valorContratoLine: null,
    desconto: null,
    lucro: null
  };

  get isCreateBatchMode(): boolean {
    return this.createEntryMode === 'BATCH';
  }

  get createBatchCount(): number {
    return this.createBatchLines.length;
  }

  get createSubmitText(): string {
    if (this.createSaving) return '';
    if (this.isCreateBatchMode) {
      const count = this.createBatchCount;
      return count > 0 ? `Cadastrar Lote (${count})` : 'Cadastrar Lote';
    }
    return 'Cadastrar';
  }

  get isCreateSaveDisabled(): boolean {
    if (this.createSaving) return true;
    if (!this.isCreateBatchMode) return false;
    if (this.createBatchCount === 0) return true;
    return this.createBatchValidationSummary.invalid > 0;
  }

  get hasBatchSelection(): boolean {
    if (this.selectedBatchLineUid == null) return false;
    return this.createBatchLines.some((x) => x.uid === this.selectedBatchLineUid);
  }

  get selectedBatchLine(): CreateBatchLineDraft | null {
    if (this.selectedBatchLineUid == null) return null;
    return this.createBatchLines.find((x) => x.uid === this.selectedBatchLineUid) ?? null;
  }

  get selectedBatchLineIndex(): number {
    if (this.selectedBatchLineUid == null) return -1;
    return this.createBatchLines.findIndex((x) => x.uid === this.selectedBatchLineUid);
  }

  get batchActiveDetailLine(): CreateBatchLineDraft | null {
    if (!this.batchDetailOpen) return null;
    return this.selectedBatchLine;
  }

  get batchValidationMessage(): string {
    const s = this.createBatchValidationSummary;
    if (!this.isCreateBatchMode) return '';
    if (s.total === 0) return 'Adicione linhas ao lote para começar o preenchimento.';
    if (s.invalid > 0) return `Corrija ${s.invalid} linha(s) inválida(s) antes de salvar.`;
    return `Lote pronto para envio: ${s.valid} linha(s) válida(s).`;
  }

  get batchMassHasPreview(): boolean {
    return !!this.batchMassPreview && (this.batchMassPreview.recognizedRows > 0 || this.batchMassPreview.parseErrors.length > 0);
  }

  get batchMassSeparatorLabel(): string {
    if (!this.batchMassPreview) return '';
    if (this.batchMassPreview.separator === 'TAB') return 'TAB';
    if (this.batchMassPreview.separator === 'PIPE') return '|';
    return ';';
  }

  get batchMassPreviewRowsPreview(): Array<{ line: number; data: Record<string, string>; errors: string[] }> {
    const rows = this.batchMassPreview?.rows ?? [];
    return rows.slice(0, 5).map((row) => ({ line: row.sourceLineNumber, data: row.data, errors: row.errors }));
  }

  get batchExcelPreviewRowsPreview(): BatchExcelPreviewRowDto[] {
    return (this.batchExcelPreview?.rows ?? []).slice(0, 8);
  }

  getBatchExcelRowErrorsTitle(row: BatchExcelPreviewRowDto | null | undefined): string {
    const errors = row?.errors ?? [];
    return errors
      .map((e) => `${e?.column ? `${e.column}: ` : ''}${e?.message ?? ''}`.trim())
      .filter(Boolean)
      .join(' | ');
  }

  getBatchExcelRowPrimaryError(row: BatchExcelPreviewRowDto | null | undefined): string {
    const first = row?.errors?.[0];
    if (!first) return '';
    return `${first.column ? `${first.column}: ` : ''}${first.message ?? ''}`.trim();
  }

  get isReservaExpandedGroup(): boolean {
    return this.filterSkil === 'RESERVA' && !!(this.expandedGroup ?? '').trim();
  }

  get isExpandedGroupNamedReserva(): boolean {
    return (this.expandedGroup ?? '').toString().trim().localeCompare('RESERVA', 'pt-BR', { sensitivity: 'base' }) === 0;
  }

  get hasGroupLineSelectionTools(): boolean {
    return !this.isClientRestricted && !!(this.expandedGroup ?? '').trim();
  }

  get canMoveSelectedLinesToReserva(): boolean {
    return this.hasGroupLineSelectionTools && !this.isReservaExpandedGroup && !this.isExpandedGroupNamedReserva;
  }

  get reservaSelectedCount(): number {
    return this.reservaSelectedLineIds.length;
  }

  get reservaSelectedLines(): LineRow[] {
    if (!this.hasGroupLineSelectionTools || this.reservaSelectedLineIds.length === 0) return [];
    const ids = new Set(this.reservaSelectedLineIds);
    return this.groupLines.filter((x) => ids.has(x.id));
  }

  get reservaTransferTargetClientsOptions(): string[] {
    const set = new Set<string>();
    for (const c of this.reservaTransferClients) {
      const v = (c ?? '').toString().trim();
      if (!v) continue;
      if (v.localeCompare('RESERVA', 'pt-BR', { sensitivity: 'base' }) === 0) continue;
      set.add(v);
    }
    const current = (this.reservaTransferModel?.clienteDestino ?? '').toString().trim();
    if (current && current.localeCompare('RESERVA', 'pt-BR', { sensitivity: 'base' }) !== 0) set.add(current);
    return Array.from(set).sort((a, b) => a.localeCompare(b, 'pt-BR', { sensitivity: 'base' }));
  }

  get isGroupMode(): boolean {
    return this.viewMode === 'GROUPS';
  }

  get isKpiLoading(): boolean {
    return this.loading || this.loadingKpis;
  }

  get hasAdditionalFiltersApplied(): boolean {
    return this.additionalMode !== 'ALL' || this.selectedAdditionalServices.length > 0;
  }

  get additionalModeLabel(): string {
    if (this.additionalMode === 'WITH') return 'Com adicionais';
    if (this.additionalMode === 'WITHOUT') return 'Sem adicionais';
    return 'Todos os adicionais';
  }

  get additionalSelectedLabels(): string[] {
    return this.selectedAdditionalServices
      .map((key) => this.additionalServiceOptions.find((x) => x.key === key)?.label ?? key);
  }

  // ✅ fecha dropdown ao clicar fora
  @HostListener('document:click', ['$event'])
  onDocumentClick(ev: MouseEvent) {
    if (!isPlatformBrowser(this.platformId)) return;

    // Se modal estiver aberto, não mexe no dropdown por clique no overlay
    if (this.anyModalOpen()) return;

    if (!this.showClientMenu && !this.showAdditionalMenu) return;
    const target = ev.target as HTMLElement | null;
    if (!target) return;

    const insideClient = !!target.closest('.client-filter-wrap');
    const insideAdditional = !!target.closest('.additional-filter-wrap');
    let changed = false;

    if (this.showClientMenu && !insideClient) {
      this.showClientMenu = false;
      changed = true;
    }

    if (this.showAdditionalMenu && !insideAdditional) {
      this.showAdditionalMenu = false;
      changed = true;
    }

    if (changed) {
      this.cdr.detectChanges();
    }
  }

  // ✅ ESC fecha dropdown OU modal (sem conflito)
  @HostListener('document:keydown', ['$event'])
  onDocumentKeydown(ev: Event) {
    if (!isPlatformBrowser(this.platformId)) return;

    const keyboard = ev as KeyboardEvent;
    if (keyboard.key === 'Escape') {
      if (this.anyModalOpen()) {
        keyboard.preventDefault();
        keyboard.stopPropagation();
        this.closeAllModals();
        return;
      }

      let changed = false;

      if (this.showClientMenu) {
        this.showClientMenu = false;
        changed = true;
      }

      if (this.showAdditionalMenu) {
        this.showAdditionalMenu = false;
        changed = true;
      }

      if (changed) {
        keyboard.stopPropagation();
        this.cdr.detectChanges();
      }
    }
  }

  ngOnDestroy(): void {
    if (this.searchTimer) clearTimeout(this.searchTimer);
    this.navigationSub?.unsubscribe();
  }

  ngOnInit(): void {
    if (!isPlatformBrowser(this.platformId)) return;
    this.isSysAdmin = this.authService.hasRole('sysadmin');
    this.isGestor = this.authService.hasRole('gestor');
    this.isClientRestricted = !(this.isSysAdmin || this.isGestor);

    if (this.isClientRestricted) {
      this.filterSkil = 'ALL';
      this.additionalMode = 'ALL';
      this.selectedAdditionalServices = [];
      this.selectedClients = [];
    }
  }

  async ngAfterViewInit() {
    if (!isPlatformBrowser(this.platformId)) return;
    this.initAnimations();

    setTimeout(() => {
      this.refreshData();
      if (!this.isClientRestricted) {
        this.loadClients();
      }
      this.loadPlanRules();
      this.loadAccountCompanies();

      const state = history.state;
      if (state && state.toastMessage) {
        const msg = String(state.toastMessage);
        const newState = { ...state };
        delete newState.toastMessage;
        history.replaceState(newState, '', location.href);
        this.showToast(msg);
      }
    });

    this.navigationSub = this.router.events
      .pipe(filter((event): event is NavigationEnd => event instanceof NavigationEnd))
      .subscribe((event) => {
        const url = (event.urlAfterRedirects || '').toLowerCase();
        if (!url.includes('/geral')) return;

        this.searchResolvedClient = null;
        if (!this.isClientRestricted) {
          this.loadClients();
        }
        this.refreshData();
      });
  }

  private initAnimations() {
    document.documentElement.classList.add('js-animate');
    setTimeout(() => {
      const items = document.querySelectorAll<HTMLElement>('[data-animate]');
      items.forEach((el) => el.classList.add('is-visible'));
    }, 100);
  }

  private async loadPlanRules() {
    try {
      await this.planAutoFill.load();
      const extraPlans = this.planAutoFill.getPlanOptions();
      if (extraPlans.length > 0) {
        this.planOptions = this.mergeOptionList(this.planOptions, extraPlans);
        this.cdr.detectChanges();
      }
    } catch {
      // silencioso: segue com a lista estática
    }
  }

  private loadAccountCompanies() {
    this.loadingAccountCompanies = true;

    this.http.get<AccountCompanyOption[]>(`${this.apiBase}/account-companies`).subscribe({
      next: (data) => {
        const normalized = this.normalizeAccountCompanies(data);
        this.accountCompanies =
          normalized.length > 0 ? normalized : [...this.fallbackAccountCompanies];
        this.loadingAccountCompanies = false;

        this.syncContaEmpresaSelection(this.createModel);
        this.syncContaEmpresaSelection(this.editModel);
        this.cdr.detectChanges();
      },
      error: () => {
        this.accountCompanies = [...this.fallbackAccountCompanies];
        this.loadingAccountCompanies = false;

        this.syncContaEmpresaSelection(this.createModel);
        this.syncContaEmpresaSelection(this.editModel);
      }
    });
  }

  // ============================================================
  // ✅ FIX PRINCIPAL: limpeza forçada de backdrops/scroll lock
  // ============================================================
  private anyModalOpen(): boolean {
    return !!(this.detailOpen || this.financeOpen || this.editOpen || this.createOpen || this.reservaTransferOpen || this.moveToReservaOpen);
  }

  private cleanupModalArtifacts() {
    if (!isPlatformBrowser(this.platformId)) return;

    // Remove qualquer backdrop “preso” (Bootstrap ou o seu custom)
    document
      .querySelectorAll('.modal-backdrop, .modal-backdrop-custom')
      .forEach((el) => el.remove());

    // Remove lock de scroll que o Bootstrap às vezes aplica
    document.body.classList.remove('modal-open');
    document.body.style.removeProperty('overflow');
    document.body.style.removeProperty('padding-right');

    // Às vezes também cai no html
    document.documentElement.style.removeProperty('overflow');
  }

  closeAllModals() {
    // Fecha tudo de forma imediata (sem await) para o overlay sumir na hora
    this.detailOpen = false;
    this.financeOpen = false;
    this.editOpen = false;
    this.createOpen = false;
    this.reservaTransferOpen = false;
    this.moveToReservaOpen = false;

    this.detailData = null;
    this.financeData = null;

    this.editSaving = false;
    this.createSaving = false;

    this.editModel = null;
    this.editingId = null;
    this.batchDetailOpen = false;
    this.batchMassPreview = null;
    this.batchExcelPreview = null;
    this.batchExcelPreviewLoading = false;
    this.batchExcelTemplateDownloading = false;
    this.reservaTransferSaving = false;
    this.moveToReservaSaving = false;
    this.reservaTransferLastResult = null;
    this.moveToReservaLastResult = null;

    // Limpa overlays/locks residuais
    this.cleanupModalArtifacts();

    // Garante que o Angular replique no DOM imediatamente
    this.cdr.detectChanges();

    // Segurança extra: um “tick” depois, limpa de novo (caso algo tenha sido inserido depois)
    setTimeout(() => this.cleanupModalArtifacts(), 0);
  }

  // ============================================================

  private withNoCache(params: HttpParams): HttpParams {
    return params.set('_ts', Date.now().toString());
  }

  refreshData(opts?: { keepCurrentPage?: boolean }) {
    const keepCurrentPage = !!opts?.keepCurrentPage;
    this.keepPageOnNextGroupsLoad = keepCurrentPage;
    if (!keepCurrentPage && this.filterSkil === 'RESERVA') {
      this.page = 1;
    }
    this.searchResolvedClient = null;
    this.loadKpis();
    this.viewMode = 'GROUPS';
    this.loadGroups();
  }

  private isSpecificSearchTerm(term: string): boolean {
    const t = (term ?? '').trim();
    if (!t) return false;

    const digits = t.replace(/\D/g, '');
    if (!digits) return false;

    if (digits.length >= 17) return true; // ICCID
    if (digits.length >= 8 && digits.length <= 14) return true; // linha

    return false;
  }

  private resolveSearchToClient(term: string): Promise<string | null> {
    const s = (term ?? '').trim();
    if (!s) return Promise.resolve(null);

    const pageSize = this.hasAdditionalFiltersApplied ? '500' : '1';
    let params = new HttpParams().set('page', '1').set('pageSize', pageSize).set('search', s);
    params = this.applyBaseFilters(params);

    if (this.selectedClients.length > 0) {
      this.selectedClients.forEach((c) => (params = params.append('client', c)));
    }

    return new Promise((resolve) => {
      this.http.get<ApiPagedResult<ApiLineList>>(this.apiBase, { params: this.withNoCache(params) }).subscribe({
        next: (res) => {
          const source = this.hasAdditionalFiltersApplied
            ? this.applyAdditionalFiltersClientSide(res.items ?? [])
            : (res.items ?? []);
          const first = source[0];
          const client = (first?.cliente ?? '').trim();
          resolve(client || null);
        },
        error: () => resolve(null)
      });
    });
  }

  onSearch() {
    if (this.searchTimer) clearTimeout(this.searchTimer);

    this.searchTimer = setTimeout(async () => {
      this.expandedGroup = null;
      this.groupLines = [];
      this.page = 1;

      const term = (this.searchTerm ?? '').trim();

      if (!term) {
        this.searchResolvedClient = null;
        this.loadKpis();
        this.loadGroups();
        return;
      }

      if (this.isSpecificSearchTerm(term)) {
        const client = await this.resolveSearchToClient(term);

        if (client) {
          this.searchResolvedClient = client;
          this.loadKpis();
          await this.loadOnlyThisClientGroup(client);
          this.expandedGroup = client;
          this.fetchGroupLines(client, term);
          return;
        }
      }

      this.searchResolvedClient = null;
      this.loadKpis();
      this.loadGroups();
    }, 300);
  }

  private loadOnlyThisClientGroup(clientName: string): Promise<void> {
    const requestVersion = ++this.groupsRequestVersion;
    this.loading = true;

    if (this.hasAdditionalFiltersApplied) {
      return this.loadOnlyThisClientGroupFromLines(clientName, requestVersion);
    }

    let params = new HttpParams().set('page', '1').set('pageSize', '9999');
    params = this.applyBaseFilters(params);

    params = params.append('client', clientName);

    return new Promise((resolve) => {
      this.http.get<ApiPagedResult<ClientGroupDto>>(`${this.apiBase}/groups`, { params: this.withNoCache(params) }).subscribe({
        next: (res) => {
          if (requestVersion !== this.groupsRequestVersion) {
            resolve();
            return;
          }

          let items = res.items || [];
          items = items.filter(
            (g) => (g.cliente || '').trim().toUpperCase() === (clientName || '').trim().toUpperCase()
          );

          this.clientGroups = this.sortGroupsWithReservaFirst(items);
          this.total = items.length;
          this.loading = false;
          this.cdr.detectChanges();
          resolve();
        },
        error: () => {
          if (requestVersion !== this.groupsRequestVersion) {
            resolve();
            return;
          }

          this.loading = false;
          this.showToast('Erro ao carregar grupos.');
          resolve();
        }
      });
    });
  }

  private async loadOnlyThisClientGroupFromLines(clientName: string, requestVersion: number): Promise<void> {
    try {
      const lines = await this.fetchLinesForGrouping();
      if (requestVersion !== this.groupsRequestVersion) return;

      const target = (clientName || '').trim().toUpperCase();
      let groups = this.buildGroupsFromLines(lines);
      groups = groups.filter((g) => (g.cliente || '').trim().toUpperCase() === target);

      this.clientGroups = this.sortGroupsWithReservaFirst(groups);
      this.total = groups.length;
      this.loading = false;
      this.cdr.detectChanges();
    } catch {
      if (requestVersion !== this.groupsRequestVersion) return;
      this.loading = false;
      this.showToast('Erro ao carregar grupos.');
    }
  }

  private loadClients() {
    const requestVersion = ++this.clientsRequestVersion;
    this.loadingClientsList = true;
    this.clientsList = [];

    if (this.hasAdditionalFiltersApplied) {
      void this.loadClientsFromLines(requestVersion);
      return;
    }

    let params = new HttpParams();
    params = this.applyBaseFilters(params);

    this.http.get<string[]>(`${this.apiBase}/clients`, { params: this.withNoCache(params) }).subscribe({
      next: (data) => {
        if (requestVersion !== this.clientsRequestVersion) return;
        this.clientsList = data || [];
        this.loadingClientsList = false;
      },
      error: () => {
        if (requestVersion !== this.clientsRequestVersion) return;
        this.loadingClientsList = false;
        console.error('Erro ao carregar lista de clientes para o filtro.');
      }
    });
  }

  private async loadClientsFromLines(requestVersion: number): Promise<void> {
    try {
      let baseParams = new HttpParams()
        .set('sortBy', 'cliente')
        .set('sortDir', 'asc');
      baseParams = this.applyBaseFilters(baseParams);

      const pageSize = 5000;
      let page = 1;
      let expectedTotal = 0;
      const allLines: ApiLineList[] = [];

      while (page <= 500) {
        const params = baseParams
          .set('page', String(page))
          .set('pageSize', String(pageSize));

        const response = await firstValueFrom(
          this.http.get<ApiPagedResult<ApiLineList>>(this.apiBase, { params: this.withNoCache(params) })
        );

        const items = response?.items ?? [];
        expectedTotal = this.toInt(response?.total);
        allLines.push(...items);

        if (items.length === 0) break;
        if (items.length < pageSize) break;
        if (expectedTotal > 0 && allLines.length >= expectedTotal) break;

        page += 1;
      }

      if (requestVersion !== this.clientsRequestVersion) return;

      const filteredLines = this.applyAdditionalFiltersClientSide(allLines);
      const fallbackClient = this.filterSkil === 'RESERVA' ? 'RESERVA' : '';
      const clients = filteredLines
        .map((x) => ((x.cliente ?? '').toString().trim()) || fallbackClient)
        .filter((x) => !!x);

      this.clientsList = Array.from(new Set(clients)).sort((a, b) =>
        a.localeCompare(b, 'pt-BR', { sensitivity: 'base' })
      );
      this.loadingClientsList = false;
    } catch {
      if (requestVersion !== this.clientsRequestVersion) return;
      this.loadingClientsList = false;
      console.error('Erro ao carregar lista de clientes para o filtro.');
    }
  }

  setFilter(type: 'ALL' | 'PF' | 'PJ' | 'RESERVA') {
    if (this.isClientRestricted && type !== 'ALL') return;

    const isSameFilter = this.filterSkil === type;

    this.expandedGroup = null;
    this.groupLines = [];
    if (!isSameFilter) {
      this.filterSkil = type;
    }

    this.selectedClients = [];
    this.clientSearchTerm = '';
    this.searchResolvedClient = null;

    if (!this.isClientRestricted) {
      this.loadClients();
    }

    this.page = 1;
    this.refreshData();
  }

  setAdditionalMode(mode: AdditionalMode) {
    if (this.isClientRestricted) return;
    if (this.additionalMode === mode) return;

    this.additionalMode = mode;
    this.expandedGroup = null;
    this.groupLines = [];
    this.searchResolvedClient = null;
    this.page = 1;

    this.loadClients();
    this.refreshData();
  }

  toggleAdditionalService(key: AdditionalServiceKey) {
    if (this.isClientRestricted) return;
    const idx = this.selectedAdditionalServices.indexOf(key);
    if (idx >= 0) this.selectedAdditionalServices.splice(idx, 1);
    else this.selectedAdditionalServices.push(key);

    this.expandedGroup = null;
    this.groupLines = [];
    this.searchResolvedClient = null;
    this.page = 1;

    this.loadClients();
    this.refreshData();
  }

  isAdditionalServiceSelected(key: AdditionalServiceKey): boolean {
    return this.selectedAdditionalServices.includes(key);
  }

  clearAdditionalFilters() {
    if (this.isClientRestricted) return;
    this.additionalMode = 'ALL';
    this.selectedAdditionalServices = [];
    this.expandedGroup = null;
    this.groupLines = [];
    this.searchResolvedClient = null;
    this.page = 1;

    this.loadClients();
    this.refreshData();
  }

  private applyBaseFilters(params: HttpParams): HttpParams {
    let next = params;

    if (this.filterSkil === 'PF') next = next.set('skil', 'PESSOA FÍSICA');
    else if (this.filterSkil === 'PJ') next = next.set('skil', 'PESSOA JURÍDICA');
    else if (this.filterSkil === 'RESERVA') next = next.set('skil', 'RESERVA');

    if (this.additionalMode === 'WITH') next = next.set('additionalMode', 'with');
    else if (this.additionalMode === 'WITHOUT') next = next.set('additionalMode', 'without');

    if (this.selectedAdditionalServices.length > 0) {
      next = next.set('additionalServices', this.selectedAdditionalServices.join(','));
    }

    return next;
  }

  private getAdditionalValue(line: ApiLineList, key: AdditionalServiceKey): number {
    const raw = key === 'gvd'
      ? line.gestaoVozDados
      : key === 'skeelo'
      ? line.skeelo
      : key === 'news'
      ? line.vivoNewsPlus
      : key === 'travel'
      ? line.vivoTravelMundo
      : key === 'sync'
      ? line.vivoSync
      : line.vivoGestaoDispositivo;

    const n = this.toNullableNumber(raw);
    return n ?? 0;
  }

  private hasAnyAdditional(line: ApiLineList): boolean {
    return (this.getAdditionalValue(line, 'gvd') > 0) ||
      (this.getAdditionalValue(line, 'skeelo') > 0) ||
      (this.getAdditionalValue(line, 'news') > 0) ||
      (this.getAdditionalValue(line, 'travel') > 0) ||
      (this.getAdditionalValue(line, 'sync') > 0) ||
      (this.getAdditionalValue(line, 'dispositivo') > 0);
  }

  private matchesAdditionalFilters(line: ApiLineList): boolean {
    const selected = this.selectedAdditionalServices;
    const hasSelected = selected.length > 0;

    if (hasSelected) {
      if (this.additionalMode === 'WITHOUT') {
        return selected.every((svc) => this.getAdditionalValue(line, svc) <= 0);
      }

      // WITH e também ALL com serviços selecionados
      return selected.some((svc) => this.getAdditionalValue(line, svc) > 0);
    }

    if (this.additionalMode === 'WITH') {
      return this.hasAnyAdditional(line);
    }

    if (this.additionalMode === 'WITHOUT') {
      return !this.hasAnyAdditional(line);
    }

    return true;
  }

  private applyAdditionalFiltersClientSide(lines: ApiLineList[]): ApiLineList[] {
    if (!Array.isArray(lines) || lines.length === 0) return [];
    return lines.filter((line) => this.matchesAdditionalFilters(line));
  }

  private loadKpis() {
    const requestVersion = ++this.kpiRequestVersion;
    this.loadingKpis = true;
    this.cdr.detectChanges();

    void this.loadKpisInternal(requestVersion);
  }

  private async loadKpisInternal(requestVersion: number) {
    try {
      const groups = await this.fetchAllGroupsForKpis();
      if (requestVersion !== this.kpiRequestVersion) return;

      if (groups.length === 0) {
        await this.loadKpisFromLines(requestVersion);
        return;
      }

      this.applyKpisFromGroups(groups);
    } catch {
      if (requestVersion !== this.kpiRequestVersion) return;
      await this.loadKpisFromLines(requestVersion);
      return;
    }

    if (requestVersion !== this.kpiRequestVersion) return;
    this.loadingKpis = false;
    this.cdr.detectChanges();
  }

  private async fetchAllGroupsForKpis(): Promise<ClientGroupDto[]> {
    if (this.hasAdditionalFiltersApplied) {
      const lines = await this.fetchLinesForGrouping();
      let groups = this.buildGroupsFromLines(lines);

      if (this.searchResolvedClient) {
        const target = (this.searchResolvedClient || '').trim().toUpperCase();
        groups = groups.filter((g) => (g.cliente || '').trim().toUpperCase() === target);
      }

      if (this.selectedClients.length > 0) {
        groups = groups.filter((g) =>
          this.selectedClients.some(
            (selected) => (selected || '').trim().toUpperCase() === (g.cliente || '').trim().toUpperCase()
          )
        );
      }

      return groups;
    }

    let baseParams = new HttpParams();
    baseParams = this.applyBaseFilters(baseParams);

    if (!this.searchResolvedClient && this.searchTerm) {
      baseParams = baseParams.set('search', this.searchTerm);
    }

    const pageSize = 2000;
    let page = 1;
    let expectedTotal = 0;
    const allGroups: ClientGroupDto[] = [];

    while (page <= 500) {
      const params = baseParams
        .set('page', String(page))
        .set('pageSize', String(pageSize));

      const response = await firstValueFrom(
        this.http.get<ApiPagedResult<ClientGroupDto>>(`${this.apiBase}/groups`, { params: this.withNoCache(params) })
      );

      const items = response?.items ?? [];
      expectedTotal = this.toInt(response?.total);
      allGroups.push(...items);

      if (items.length === 0) break;
      if (items.length < pageSize) break;
      if (expectedTotal > 0 && allGroups.length >= expectedTotal) break;

      page += 1;
    }

    if (this.searchResolvedClient) {
      const target = (this.searchResolvedClient || '').trim().toUpperCase();
      return allGroups.filter((g) => (g.cliente || '').trim().toUpperCase() === target);
    }

    if (this.selectedClients.length > 0) {
      return allGroups.filter((g) =>
        this.selectedClients.some(
          (selected) => (selected || '').trim().toUpperCase() === (g.cliente || '').trim().toUpperCase()
        )
      );
    }

    return allGroups;
  }

  private async loadKpisFromLines(requestVersion: number = this.kpiRequestVersion) {
    try {
      const lines = await this.fetchLinesForGrouping();
      let groups = this.buildGroupsFromLines(lines);

      if (this.searchResolvedClient) {
        const target = (this.searchResolvedClient || '').trim().toUpperCase();
        groups = groups.filter((g) => (g.cliente || '').trim().toUpperCase() === target);
      } else if (this.selectedClients.length > 0) {
        groups = groups.filter((g) =>
          this.selectedClients.some(
            (selected) =>
              (selected || '').trim().toUpperCase() === (g.cliente || '').trim().toUpperCase()
          )
        );
      }

      if (requestVersion !== this.kpiRequestVersion) return;
      this.applyKpisFromGroups(groups);
    } catch {
      if (requestVersion !== this.kpiRequestVersion) return;
      this.applyKpisFromGroups([]);
    } finally {
      if (requestVersion !== this.kpiRequestVersion) return;
      this.loadingKpis = false;
      this.cdr.detectChanges();
    }
  }

  private applyKpisFromGroups(groups: ClientGroupDto[]): void {
    const safe = Array.isArray(groups) ? groups : [];
    this.kpiTotalClientes = safe.length;
    this.kpiTotalLinhas = safe.reduce((acc, group) => acc + this.toInt(group?.totalLinhas), 0);
    this.kpiAtivas = safe.reduce((acc, group) => acc + this.toInt(group?.ativos), 0);
    this.kpiBloqueadas = safe.reduce((acc, group) => acc + this.toInt(group?.bloqueados), 0);
  }


  private loadGroups() {
    const requestVersion = ++this.groupsRequestVersion;
    this.loading = true;

    const hasSelection = this.selectedClients.length > 0;
    const hasResolved = !!this.searchResolvedClient;
    const keepCurrentPage = this.keepPageOnNextGroupsLoad;
    this.keepPageOnNextGroupsLoad = false;

    if (!keepCurrentPage && this.filterSkil === 'RESERVA' && !hasSelection && !hasResolved) {
      this.page = 1;
    }

    if (this.hasAdditionalFiltersApplied) {
      void this.loadGroupsFromLines(hasSelection, hasResolved, requestVersion);
      return;
    }

    const pageToLoad = (hasSelection || hasResolved) ? '1' : String(this.page);
    const sizeToLoad = (hasSelection || hasResolved) ? '9999' : String(this.pageSize);

    let params = new HttpParams().set('page', pageToLoad).set('pageSize', sizeToLoad);
    params = this.applyBaseFilters(params);

    if (!hasResolved && this.searchTerm) params = params.set('search', this.searchTerm);

    if (hasResolved) {
      params = params.append('client', this.searchResolvedClient!);
    } else if (hasSelection) {
      this.selectedClients.forEach((c) => (params = params.append('client', c)));
    }

    this.http.get<ApiPagedResult<ClientGroupDto>>(`${this.apiBase}/groups`, { params: this.withNoCache(params) }).subscribe({
      next: (res) => {
        if (requestVersion !== this.groupsRequestVersion) return;

        let items = res.items || [];

        if (hasResolved) {
          const target = (this.searchResolvedClient || '').trim().toUpperCase();
          items = items.filter((g) => (g.cliente || '').trim().toUpperCase() === target);
          this.total = items.length;
        } else if (hasSelection) {
          items = items.filter((g) =>
            this.selectedClients.some(
              (selected) =>
                (selected || '').trim().toUpperCase() === (g.cliente || '').trim().toUpperCase()
            )
          );
          this.total = items.length;
        } else {
          this.total = res.total;
        }

        if (items.length === 0) {
          this.loadGroupsFromLines(hasSelection, hasResolved, requestVersion);
          return;
        }

        this.clientGroups = this.sortGroupsWithReservaFirst(items);
        this.loading = false;
        this.cdr.detectChanges();
      },
      error: () => {
        if (requestVersion !== this.groupsRequestVersion) return;
        this.loadGroupsFromLines(hasSelection, hasResolved, requestVersion);
      }
    });
  }

  private async loadGroupsFromLines(hasSelection: boolean, hasResolved: boolean, requestVersion: number) {
    try {
      const lines = await this.fetchLinesForGrouping();
      if (requestVersion !== this.groupsRequestVersion) return;
      let groups = this.buildGroupsFromLines(lines);

      if (hasResolved) {
        const target = (this.searchResolvedClient || '').trim().toUpperCase();
        groups = groups.filter((g) => (g.cliente || '').trim().toUpperCase() === target);
      } else if (hasSelection) {
        groups = groups.filter((g) =>
          this.selectedClients.some(
            (selected) =>
              (selected || '').trim().toUpperCase() === (g.cliente || '').trim().toUpperCase()
          )
        );
      }

      this.total = groups.length;

      if (!hasSelection && !hasResolved) {
        const start = Math.max(0, (this.page - 1) * this.pageSize);
        groups = groups.slice(start, start + this.pageSize);
      }

      this.clientGroups = this.sortGroupsWithReservaFirst(groups);
    } catch {
      if (requestVersion !== this.groupsRequestVersion) return;
      this.clientGroups = [];
      this.total = 0;
      this.showToast('Erro ao carregar grupos.');
    } finally {
      if (requestVersion !== this.groupsRequestVersion) return;
      this.loading = false;
      this.cdr.detectChanges();
    }
  }

  private async fetchLinesForGrouping(): Promise<ApiLineList[]> {
    let baseParams = new HttpParams()
      .set('sortBy', 'cliente')
      .set('sortDir', 'asc');
    baseParams = this.applyBaseFilters(baseParams);

    if (this.searchResolvedClient) {
      baseParams = baseParams.set('client', this.searchResolvedClient);
    } else if (this.searchTerm) {
      baseParams = baseParams.set('search', this.searchTerm);
    }

    const pageSize = 5000;
    let page = 1;
    let expectedTotal = 0;
    const allLines: ApiLineList[] = [];

    while (page <= 500) {
      const params = baseParams
        .set('page', String(page))
        .set('pageSize', String(pageSize));

      const response = await firstValueFrom(
        this.http.get<ApiPagedResult<ApiLineList>>(this.apiBase, { params: this.withNoCache(params) })
      );

      const items = response?.items ?? [];
      expectedTotal = this.toInt(response?.total);
      allLines.push(...items);

      if (items.length === 0) break;
      if (items.length < pageSize) break;
      if (expectedTotal > 0 && allLines.length >= expectedTotal) break;

      page += 1;
    }

    return this.applyAdditionalFiltersClientSide(allLines);
  }

  private buildGroupsFromLines(lines: ApiLineList[]): ClientGroupDto[] {
    const grouped = new Map<string, ClientGroupDto>();
    const fallbackClient = this.filterSkil === 'RESERVA' ? 'RESERVA' : 'SEM CLIENTE';

    for (const row of lines ?? []) {
      const client = ((row?.cliente ?? '').toString().trim()) || fallbackClient;
      const key = client.toUpperCase();

      let group = grouped.get(key);
      if (!group) {
        group = {
          cliente: client,
          totalLinhas: 0,
          ativos: 0,
          bloqueados: 0
        };
        grouped.set(key, group);
      }

      group.totalLinhas += 1;

      const status = ((row?.status ?? '').toString().trim()).toLowerCase();
      if (status.includes('ativo')) group.ativos += 1;
      if (status.includes('bloque') || status.includes('perda') || status.includes('roubo')) {
        group.bloqueados += 1;
      }
    }

    return this.sortGroupsWithReservaFirst(Array.from(grouped.values()));
  }

  private sortGroupsWithReservaFirst(groups: ClientGroupDto[]): ClientGroupDto[] {
    const list = Array.isArray(groups) ? [...groups] : [];
    return list.sort((a, b) => {
      const aReserva = (a?.cliente || '').trim().localeCompare('RESERVA', 'pt-BR', { sensitivity: 'base' }) === 0;
      const bReserva = (b?.cliente || '').trim().localeCompare('RESERVA', 'pt-BR', { sensitivity: 'base' }) === 0;
      if (aReserva !== bReserva) return aReserva ? -1 : 1;
      return (a?.cliente || '').localeCompare((b?.cliente || ''), 'pt-BR', { sensitivity: 'base' });
    });
  }

  toggleGroup(clientName: string) {
    if (this.expandedGroup === clientName) {
      this.expandedGroup = null;
      this.groupLines = [];
      this.clearReservaSelection();
      return;
    }
    this.clearReservaSelection();
    this.expandedGroup = clientName;

    const term = (this.searchTerm ?? '').trim();
    const useTerm = term && this.isSpecificSearchTerm(term) ? term : undefined;

    this.fetchGroupLines(clientName, useTerm);
  }

  fetchGroupLines(clientName: string, search?: string) {
    const requestVersion = ++this.linesRequestVersion;
    this.groupLines = [];
    this.clearReservaSelection();
    this.loadingLines = true;

    let params = new HttpParams()
      .set('client', clientName)
      .set('page', '1')
      .set('pageSize', '500')
      .set('sortBy', 'item')
      .set('sortDir', 'asc');
    params = this.applyBaseFilters(params);

    if (search) params = params.set('search', search);

    this.http.get<ApiPagedResult<ApiLineList>>(this.apiBase, { params: this.withNoCache(params) }).subscribe({
      next: (res) => {
        if (requestVersion !== this.linesRequestVersion) return;
        const filteredItems = this.applyAdditionalFiltersClientSide(res.items ?? []);
        this.groupLines = filteredItems.map((x) => ({
          id: x.id,
          item: String(x.item ?? ''),
          linha: x.linha ?? '',
          chip: x.chip ?? '',
          cliente: x.cliente ?? '',
          usuario: x.usuario ?? '',
          status: x.status ?? '',
          skil: x.skil ?? '',
          contrato: x.vencConta ?? ''
        }));
        this.loadingLines = false;
      },
      error: () => {
        if (requestVersion !== this.linesRequestVersion) return;
        this.loadingLines = false;
        this.showToast('Erro ao carregar linhas do grupo.');
      }
    });
  }

  toggleClientMenu() {
    if (this.isClientRestricted) return;
    if (!this.showClientMenu) this.showAdditionalMenu = false;
    this.showClientMenu = !this.showClientMenu;
  }

  toggleAdditionalMenu() {
    if (this.isClientRestricted) return;
    if (!this.showAdditionalMenu) this.showClientMenu = false;
    this.showAdditionalMenu = !this.showAdditionalMenu;
  }

  closeClientDropdown() {
    this.showClientMenu = false;
  }

  closeAdditionalDropdown() {
    this.showAdditionalMenu = false;
  }

  closeFilterDropdowns() {
    this.showClientMenu = false;
    this.showAdditionalMenu = false;
  }

  selectClient(client: string | null) {
    if (this.isClientRestricted) return;
    if (client === null) {
      this.selectedClients = [];
    } else {
      const idx = this.selectedClients.indexOf(client);
      if (idx >= 0) this.selectedClients.splice(idx, 1);
      else this.selectedClients.push(client);
    }

    this.page = 1;
    this.expandedGroup = null;
    this.searchResolvedClient = null;
    this.refreshData();
  }

  removeClient(client: string, event: Event) {
    if (this.isClientRestricted) return;
    event.stopPropagation();
    const idx = this.selectedClients.indexOf(client);
    if (idx >= 0) {
      this.selectedClients.splice(idx, 1);
      this.page = 1;

      this.searchResolvedClient = null;
      this.refreshData();
    }
  }

  clearClientSelection(event?: Event) {
    if (this.isClientRestricted) return;
    if (event) event.stopPropagation();
    this.selectedClients = [];
    this.clientSearchTerm = '';
    this.page = 1;

    this.searchResolvedClient = null;
    this.refreshData();
  }

  isClientSelected(client: string): boolean {
    return this.selectedClients.includes(client);
  }

  get filteredClientsList(): string[] {
    if (!this.clientSearchTerm) return this.clientsList;
    const s = this.clientSearchTerm.toLowerCase();
    return this.clientsList.filter((c) => c.toLowerCase().includes(s));
  }

  setSort(key: keyof LineRow) {
    if (this.sortKey === key) this.sortDir = this.sortDir === 'asc' ? 'desc' : 'asc';
    else {
      this.sortKey = key;
      this.sortDir = 'asc';
    }
    this.page = 1;
    this.loadGroups();
  }

  onPageSizeChange() {
    this.page = 1;
    this.refreshData();
  }

  goToPage(p: number) {
    this.page = Math.max(1, Math.min(this.totalPages, p));
    this.refreshData({ keepCurrentPage: true });
  }

  trackById(_: number, row: LineRow) {
    return row.id;
  }

  get pagedRows() {
    return this.rows;
  }

  get totalPages() {
    if (this.selectedClients.length > 0) return 1;
    if (this.searchResolvedClient) return 1;
    return Math.ceil((this.total || 0) / this.pageSize) || 1;
  }

  get filteredCount() {
    return this.total || 0;
  }

  get pageStart() {
    return this.filteredCount === 0 ? 0 : (this.page - 1) * this.pageSize + 1;
  }

  get pageEnd() {
    if (this.selectedClients.length > 0 || this.searchResolvedClient) {
      return this.filteredCount;
    }

    return this.filteredCount === 0
      ? 0
      : Math.min(
          (this.page - 1) * this.pageSize +
            (this.isGroupMode ? this.clientGroups.length : this.rows.length),
          this.filteredCount
        );
  }

  get pageNumbers() {
    const total = this.totalPages;
    const current = this.page;
    const max = 5;
    let start = Math.max(1, current - 2);
    let end = Math.min(total, start + (max - 1));
    start = Math.max(1, end - (max - 1));
    const pages: number[] = [];
    for (let i = start; i <= end; i++) pages.push(i);
    return pages;
  }

  clearSearch() {
    this.searchTerm = '';
    this.searchResolvedClient = null;
    this.expandedGroup = null;
    this.groupLines = [];
    this.page = 1;
    this.refreshData();
  }

  async onImportExcel() {
    if (!this.isSysAdmin) {
      await this.showToast('Você não tem permissão para importar planilha.');
      return;
    }

    if (!this.excelInput?.nativeElement) return;
    this.excelInput.nativeElement.value = '';
    this.excelInput.nativeElement.click();
  }

  onExcelSelected(ev: Event) {
    if (!this.isSysAdmin) return;

    const file = (ev.target as HTMLInputElement).files?.[0];
    if (!file) return;

    const form = new FormData();
    form.append('file', file);

    this.loading = true;

    this.http.post<{ imported: number }>(`${this.apiBase}/import-excel`, form).subscribe({
      next: async (r) => {
        await this.showToast(`Sucesso! ${r?.imported ?? 0} registros importados.`);
        this.page = 1;
        this.refreshData();
      },
      error: async () => {
        this.loading = false;
        await this.showToast('Falha ao importar planilha.');
      }
    });
  }

  private getById(id: string, cb: (d: any) => void) {
    this.http.get(`${this.apiBase}/${id}`).subscribe({
      next: cb,
      error: () => this.showToast('Erro ao carregar detalhes.')
    });
  }

  async onDetalhes(r: LineRow) {
    this.detailOpen = true;
    this.detailData = null;
    this.cdr.detectChanges();
    this.getById(r.id, (d) => {
      this.detailData = d;
      this.cdr.detectChanges();
    });
  }

  async onFinanceiro(r: LineRow) {
    this.financeOpen = true;
    this.financeData = null;
    this.cdr.detectChanges();
    this.getById(r.id, (d) => {
      this.financeData = d;
      this.cdr.detectChanges();
    });
  }

  async onEditar(r: LineRow) {
    this.editOpen = true;
    this.editSaving = false;
    this.editModel = null;
    this.editingId = r.id;

    this.cdr.detectChanges();

    this.http.get<ApiLineDetail>(`${this.apiBase}/${r.id}`).subscribe({
      next: (d) => {
        this.editModel = this.toEditModel(d);
        this.syncContaEmpresaSelection(this.editModel);
        this.cdr.detectChanges();
      },
      error: async () => {
        this.closeAllModals();
        await this.showToast('Erro ao carregar dados para edição.');
      }
    });
  }

  onPlanoChange(isEdit: boolean) {
    const model = isEdit ? this.editModel : this.createModel;
    if (!model) return;

    const plan = (model.planoContrato ?? '').toString().trim();
    if (!plan) return;

    const suggestion = this.planAutoFill.suggest(plan);
    if (!suggestion) return;

    if (suggestion.franquiaGb != null) {
      model.franquiaVivo = suggestion.franquiaGb;
      if (model.franquiaLine === null || model.franquiaLine === undefined || model.franquiaLine === '') {
        model.franquiaLine = suggestion.franquiaGb;
      }
    }

    if (suggestion.valorPlano != null) {
      model.valorPlanoVivo = suggestion.valorPlano;
    }

    this.onFinancialChange(isEdit);
  }

  calculateFinancials(model: any) {
    if (!model) return;

    const parse = (v: any): number => {
      if (v === null || v === undefined || v === '') return 0;
      if (typeof v === 'number') return v;
      return parseFloat(v.toString().replace(',', '.')) || 0;
    };

    const valorPlano = parse(model.valorPlanoVivo);
    const gestaoVoz = parse(model.gestaoVozDados);
    const skeelo = parse(model.skeelo);
    const news = parse(model.vivoNewsPlus);
    const travel = parse(model.vivoTravelMundo);
    const gestaoDisp = parse(model.vivoGestaoDispositivo);
    const vivoSync = parse(model.vivoSync);

    const totalVivo = valorPlano + gestaoVoz + skeelo + news + travel + gestaoDisp + vivoSync;
    model.valorContratoVivo = parseFloat(totalVivo.toFixed(2));

    const totalLineManual = parse(model.valorContratoLine);
    const lucro = totalLineManual - totalVivo;
    model.lucro = parseFloat(lucro.toFixed(2));

    model.desconto = model.desconto ? parse(model.desconto) : 0.0;
  }

  onFinancialChange(isEdit: boolean) {
    const model = isEdit ? this.editModel : this.createModel;
    this.calculateFinancials(model);
  }

  async saveEdit() {
    if (!this.editingId || !this.editModel) return;

    this.editSaving = true;
    this.calculateFinancials(this.editModel);

    const { contaEmpresa: _contaEmpresa, ...editModelPayload } = this.editModel;

    const payload: UpdateMobileLineRequest = {
      ...editModelPayload,
      item: this.toInt(this.editModel.item),
      dataBloqueio: this.dateInputToIso(this.editModel.dataBloqueio),
      dataEntregaOpera: this.dateInputToIso(this.editModel.dataEntregaOpera),
      dataEntregaCliente: this.dateInputToIso(this.editModel.dataEntregaCliente),
      dtEfetivacaoServico: this.dateInputToIso(this.editModel.dtEfetivacaoServico),
      dtTerminoFidelizacao: this.dateInputToIso(this.editModel.dtTerminoFidelizacao),
      vencConta: (this.editModel.vencConta ?? '').toString(),
      franquiaVivo: this.toNullableNumber(this.editModel.franquiaVivo),
      valorPlanoVivo: this.toNullableNumber(this.editModel.valorPlanoVivo),
      gestaoVozDados: this.toNullableNumber(this.editModel.gestaoVozDados),
      skeelo: this.toNullableNumber(this.editModel.skeelo),
      vivoNewsPlus: this.toNullableNumber(this.editModel.vivoNewsPlus),
      vivoTravelMundo: this.toNullableNumber(this.editModel.vivoTravelMundo),
      vivoGestaoDispositivo: this.toNullableNumber(this.editModel.vivoGestaoDispositivo),
      vivoSync: this.toNullableNumber(this.editModel.vivoSync),
      valorContratoVivo: this.toNullableNumber(this.editModel.valorContratoVivo),
      franquiaLine: this.toNullableNumber(this.editModel.franquiaLine),
      franquiaGestao: this.toNullableNumber(this.editModel.franquiaGestao),
      locacaoAp: this.toNullableNumber(this.editModel.locacaoAp),
      valorContratoLine: this.toNullableNumber(this.editModel.valorContratoLine),
      desconto: this.toNullableNumber(this.editModel.desconto),
      lucro: this.toNullableNumber(this.editModel.lucro),
      tipoDeChip: (this.editModel.tipoDeChip ?? '').toString()
    };

    this.http.put(`${this.apiBase}/${this.editingId}`, payload).subscribe({
      next: async () => {
        this.editSaving = false;

        // fecha e limpa overlay SEMPRE
        this.closeAllModals();

        await this.showToast('Registro atualizado!');

        if (this.isGroupMode && this.expandedGroup) {
          const term = (this.searchTerm ?? '').trim();
          const useTerm = term && this.isSpecificSearchTerm(term) ? term : undefined;

          this.fetchGroupLines(this.expandedGroup, useTerm);
          this.loadGroups();
          this.loadKpis();
        } else {
          this.refreshData();
        }
      },
      error: async () => {
        this.editSaving = false;
        await this.showToast('Erro ao salvar.');
      }
    });
  }

  async onRemover(r: LineRow, fromGroup = false) {
    if (!this.isSysAdmin) {
      await this.showToast('Apenas sysadmin pode remover linhas.');
      return;
    }

    if (!(await confirmDeletionWithTyping(`a linha ${r.linha}`))) return;

    this.loading = true;

    this.http.delete(`${this.apiBase}/${r.id}`).subscribe({
      next: async () => {
        await this.showToast('Removido com sucesso.');
        this.loading = false;

        if (fromGroup && this.expandedGroup) {
          const term = (this.searchTerm ?? '').trim();
          const useTerm = term && this.isSpecificSearchTerm(term) ? term : undefined;

          this.fetchGroupLines(this.expandedGroup, useTerm);
          this.loadGroups();
          this.loadKpis();
        } else {
          this.refreshData();
        }
      },
      error: async () => {
        this.loading = false;
        await this.showToast('Erro ao remover.');
      }
    });
  }

  async onCadastrarLinha() {
    if (this.isClientRestricted) {
      await this.showToast('Você não tem permissão para cadastrar novos clientes.');
      return;
    }

    this.createMode = 'NEW_CLIENT';
    this.resetCreateModel();
    this.createOpen = true;
    void this.loadCreateReservaLines();
    this.cdr.detectChanges();
  }

  async onAddLineToGroup(clientName: string) {
    if (this.isClientRestricted) {
      await this.showToast('Você não tem permissão para adicionar linhas.');
      return;
    }

    this.createMode = 'NEW_LINE_IN_GROUP';
    this.resetCreateModel();

    this.createModel.cliente = clientName;

    if (this.filterSkil === 'PJ') this.createModel.skil = 'PESSOA JURÍDICA';
    else if (this.filterSkil === 'RESERVA') this.createModel.skil = 'RESERVA';

    this.syncContaEmpresaSelection(this.createModel);

    this.createOpen = true;
    void this.loadCreateReservaLines();
    this.cdr.detectChanges();
  }

  private resetCreateModel() {
    this.createModel = {
      cliente: '',
      docType: 'PF',
      docNumber: '',
      contaEmpresa: '',
      reservaLineId: '',
      linha: '',
      chip: '',
      tipoDeChip: '',
      usuario: '',
      status: '',
      planoContrato: '',
      conta: '',
      vencConta: '',
      skil: 'PESSOA FÍSICA',
      modalidade: '',
      item: 0,
      cedente: '',
      solicitante: '',
      dataBloqueio: '',
      dataEntregaOpera: '',
      dataEntregaCliente: '',
      dtEfetivacaoServico: '',
      dtTerminoFidelizacao: '',
      franquiaVivo: null,
      valorPlanoVivo: null,
      gestaoVozDados: null,
      skeelo: null,
      vivoNewsPlus: null,
      vivoTravelMundo: null,
      vivoGestaoDispositivo: null,
      vivoSync: null,
      valorContratoVivo: null,
      franquiaLine: null,
      franquiaGestao: null,
      locacaoAp: null,
      valorContratoLine: null,
      desconto: null,
      lucro: null
    };
    this.createEntryMode = 'SINGLE';
    this.createBatchLines = [];
    this.selectedBatchLineUid = null;
    this.batchDetailOpen = false;
    this.batchMassInputText = '';
    this.batchMassSeparatorMode = 'AUTO';
    this.batchMassPreview = null;
    this.batchExcelPreview = null;
    this.batchExcelPreviewLoading = false;
    this.createBatchValidationByUid = {};
    this.createBatchValidationSummary = { total: 0, valid: 0, invalid: 0, duplicates: 0 };
    this.createSaving = false;
  }

  setCreateEntryMode(mode: CreateEntryMode) {
    this.createEntryMode = mode;
    if (mode === 'BATCH') {
      if (this.createBatchLines.length > 0) {
        if (this.selectedBatchLineUid == null) {
          this.selectedBatchLineUid = this.createBatchLines[0]?.uid ?? null;
        }
        this.batchDetailOpen = true;
        this.ensureBatchLineDefaults(this.selectedBatchLine);
      } else {
        this.selectedBatchLineUid = null;
        this.batchDetailOpen = false;
      }
      this.recomputeBatchValidation();
      return;
    }

    this.batchDetailOpen = false;
    this.recomputeBatchValidation();
  }

  addBatchLine(seed?: Partial<CreateBatchLineDraft>) {
    const templateSource = this.selectedBatchLine ?? this.createBatchLines[this.createBatchLines.length - 1] ?? this.createModel;
    const row = this.createBatchDraftFromSource(templateSource, seed, {
      keepLinha: false,
      keepChip: false,
      copyDetails: false
    });

    this.createBatchLines = [...this.createBatchLines, row];
    this.selectedBatchLineUid = row.uid;
    this.recomputeBatchValidation();
  }

  addBatchLines(count: number) {
    const total = Math.max(1, Math.min(200, Math.floor(Number(count) || 1)));
    for (let i = 0; i < total; i++) this.addBatchLine();
  }

  removeBatchLine(uid: number) {
    this.createBatchLines = this.createBatchLines.filter((x) => x.uid !== uid);
    if (this.selectedBatchLineUid === uid) {
      this.selectedBatchLineUid = this.createBatchLines[this.createBatchLines.length - 1]?.uid ?? null;
    }
    if (this.createBatchLines.length === 0) this.batchDetailOpen = false;
    this.recomputeBatchValidation();
  }

  clearBatchLines() {
    this.createBatchLines = [];
    this.selectedBatchLineUid = null;
    this.batchDetailOpen = false;
    this.recomputeBatchValidation();
  }

  onBatchMassInputChange() {
    this.batchMassPreview = null;
  }

  previewBatchMassInput() {
    this.batchMassPreview = buildBatchMassPreview(this.batchMassInputText, {
      separatorMode: this.batchMassSeparatorMode,
      defaults: this.getBatchMassDefaults(),
      detectHeader: true
    });
  }

  clearBatchMassInput() {
    this.batchMassInputText = '';
    this.batchMassPreview = null;
  }

  useBatchMassExample() {
    this.batchMassInputText = buildBatchMassExampleText(this.batchMassSeparatorMode, true);
    this.batchMassPreview = null;
  }

  useBatchMassHeaderTemplate() {
    this.batchMassInputText = buildBatchMassHeaderLine(this.batchMassSeparatorMode);
    this.batchMassPreview = null;
  }

  async applyBatchMassInput(mode: BatchMassApplyMode) {
    if (!this.batchMassInputText.trim()) {
      await this.showToast('Cole ou digite as linhas no campo de entrada em massa.');
      return;
    }

    this.previewBatchMassInput();
    const preview = this.batchMassPreview;
    if (!preview || preview.recognizedRows <= 0) {
      await this.showToast(preview?.parseErrors[0] ?? 'Nenhuma linha reconhecida na entrada em massa.');
      return;
    }

    const parsedRows = preview.rows.map((previewRow) =>
      this.createBatchDraftFromSource(
        this.createModel,
        {
          ...previewRow.data
        } as Partial<CreateBatchLineDraft>,
        { keepLinha: true, keepChip: true, copyDetails: false }
      )
    );

    this.createBatchLines = mergeMassRows(this.createBatchLines, parsedRows, mode);
    this.selectedBatchLineUid = parsedRows[parsedRows.length - 1]?.uid ?? this.selectedBatchLineUid;
    this.batchDetailOpen = this.createBatchLines.length > 0;
    this.recomputeBatchValidation();

    await this.showToast(
      mode === 'REPLACE'
        ? `${parsedRows.length} linha(s) carregada(s) (substituindo o lote atual).`
        : `${parsedRows.length} linha(s) adicionada(s) ao lote.`
    );
  }

  selectBatchLine(uid: number) {
    this.selectedBatchLineUid = uid;
  }

  openBatchLineDetails(uid: number) {
    this.selectBatchLine(uid);
    this.batchDetailOpen = true;
    this.ensureBatchLineDefaults(this.selectedBatchLine);
    this.recomputeBatchValidation();
  }

  closeBatchLineDetails() {
    this.batchDetailOpen = false;
  }

  selectPreviousBatchLine() {
    const idx = this.selectedBatchLineIndex;
    if (idx <= 0) return;
    const prev = this.createBatchLines[idx - 1];
    if (!prev) return;
    this.openBatchLineDetails(prev.uid);
  }

  selectNextBatchLine() {
    const idx = this.selectedBatchLineIndex;
    if (idx < 0 || idx >= this.createBatchLines.length - 1) return;
    const next = this.createBatchLines[idx + 1];
    if (!next) return;
    this.openBatchLineDetails(next.uid);
  }

  duplicateLastBatchLine() {
    const source = this.createBatchLines[this.createBatchLines.length - 1];
    if (!source) return;
    this.duplicateBatchLine(source.uid);
  }

  duplicateSelectedBatchLine() {
    if (this.selectedBatchLineUid == null) return;
    this.duplicateBatchLine(this.selectedBatchLineUid);
  }

  duplicateBatchLine(uid: number) {
    const idx = this.createBatchLines.findIndex((x) => x.uid === uid);
    if (idx < 0) return;

    const source = this.createBatchLines[idx];
    const clone = this.createBatchDraftFromSource(source, undefined, {
      keepLinha: true,
      keepChip: true,
      copyDetails: true
    });

    const next = [...this.createBatchLines];
    next.splice(idx + 1, 0, clone);
    this.createBatchLines = next;
    this.selectedBatchLineUid = clone.uid;
    this.recomputeBatchValidation();
  }

  async removeInvalidBatchLines() {
    if (this.createBatchLines.length === 0) return;
    const before = this.createBatchLines.length;
    this.createBatchLines = this.createBatchLines.filter((row) => (this.getBatchValidation(row.uid)?.errors.length ?? 0) === 0);
    const removed = before - this.createBatchLines.length;
    if (removed <= 0) return;

    if (this.selectedBatchLineUid != null && !this.createBatchLines.some((x) => x.uid === this.selectedBatchLineUid)) {
      this.selectedBatchLineUid = this.createBatchLines[this.createBatchLines.length - 1]?.uid ?? null;
    }

    this.recomputeBatchValidation();
    await this.showToast(`${removed} linha(s) inválida(s) removida(s) do lote.`);
  }

  onBatchLineDraftChange() {
    this.recomputeBatchValidation();
  }

  onBatchLineFieldChange(uid: number) {
    this.selectBatchLine(uid);
    this.onBatchLineDraftChange();
  }

  onBatchLineDetailsChange() {
    this.onBatchLineDraftChange();
  }

  applySelectedBatchLineDetailsToAll() {
    const source = this.selectedBatchLine;
    if (!source) return;

    const sourceData = this.getBatchLineDataWithoutInternal(source);
    this.createBatchLines = this.createBatchLines.map((row) => {
      if (row.uid === source.uid) return row;
      return {
        ...row,
        ...sourceData,
        uid: row.uid,
        linha: row.linha,
        chip: row.chip
      };
    });

    this.recomputeBatchValidation();
  }

  trackBatchLine(_index: number, row: CreateBatchLineDraft): number {
    return row.uid;
  }

  isBatchLineSelected(uid: number): boolean {
    return this.selectedBatchLineUid === uid;
  }

  getBatchValidation(uid: number): BatchLineValidation | null {
    return this.createBatchValidationByUid[uid] ?? null;
  }

  getBatchLineErrors(uid: number): string[] {
    return this.createBatchValidationByUid[uid]?.errors ?? [];
  }

  hasBatchLineError(uid: number): boolean {
    return (this.createBatchValidationByUid[uid]?.errors.length ?? 0) > 0;
  }

  hasBatchFieldError(uid: number, field: 'linha' | 'chip'): boolean {
    const errors = this.createBatchValidationByUid[uid]?.errors ?? [];
    if (field === 'linha') {
      return errors.some((e) => e.toLowerCase().includes('linha'));
    }
    return errors.some((e) => e.toLowerCase().includes('chip'));
  }

  hasBatchDetailError(uid: number): boolean {
    const errors = this.createBatchValidationByUid[uid]?.errors ?? [];
    const detailKeywords = ['empresa', 'conta', 'plano', 'status', 'efetivação', 'fidelização', 'detalhes'];
    return errors.some((e) => detailKeywords.some((k) => e.toLowerCase().includes(k)));
  }

  hasBatchRequiredFieldError(uid: number, fieldKey: string): boolean {
    const errors = this.createBatchValidationByUid[uid]?.errors ?? [];
    const key = fieldKey.toLowerCase();
    if (key === 'conta') {
      return errors.some((e) => e.toLowerCase().startsWith('conta obrigatória'));
    }
    if (key === 'empresa') {
      return errors.some((e) => e.toLowerCase().startsWith('empresa (conta) obrigatória'));
    }
    return errors.some((e) => e.toLowerCase().includes(key));
  }

  getContaEmpresaOptionsForBatchLine(row: any): string[] {
    return this.mergeOption(row?.contaEmpresa, this.contaEmpresaOptions);
  }

  getContaOptionsForBatchLine(row: any): string[] {
    const empresaSelecionada = (row?.contaEmpresa ?? '').toString().trim();
    const baseOptions = empresaSelecionada ? this.getContasByEmpresa(empresaSelecionada) : this.getAllContas();
    return this.mergeOption(row?.conta, baseOptions);
  }

  getPlanOptionsForBatchLine(row: any): string[] {
    return this.mergeOption(row?.planoContrato, this.planOptions);
  }

  getStatusOptionsForBatchLine(row: any): string[] {
    return this.mergeOption(row?.status, this.statusOptions);
  }

  getSkilOptionsForBatchLine(row: any): string[] {
    return this.mergeOption(row?.skil, this.skilOptions);
  }

  onBatchContaEmpresaChange(row: any) {
    if (!row) return;
    const contas = this.getContasByEmpresa(row.contaEmpresa);
    const selectedConta = (row.conta ?? '').toString().trim();
    if (selectedConta) {
      const hasMatch = contas.some((c) => this.sameConta(c, selectedConta));
      if (!hasMatch) row.conta = '';
    }
    this.onBatchLineDetailsChange();
  }

  onBatchPlanoChange(row: any) {
    if (!row) return;
    const plan = (row.planoContrato ?? '').toString().trim();
    if (!plan) {
      this.onBatchLineDetailsChange();
      return;
    }

    const suggestion = this.planAutoFill.suggest(plan);
    if (suggestion) {
      if (suggestion.franquiaGb != null) {
        row.franquiaVivo = suggestion.franquiaGb;
        if (row.franquiaLine === null || row.franquiaLine === undefined || row.franquiaLine === '') {
          row.franquiaLine = suggestion.franquiaGb;
        }
      }
      if (suggestion.valorPlano != null) row.valorPlanoVivo = suggestion.valorPlano;
    }

    this.calculateFinancials(row);
    this.onBatchLineDetailsChange();
  }

  onBatchFinancialChange(row: any) {
    if (!row) return;
    this.calculateFinancials(row);
    this.onBatchLineDetailsChange();
  }

  private getBatchMassDefaults() {
    return {
      usuario: (this.createModel?.usuario ?? '').toString().trim(),
      tipoDeChip: (this.createModel?.tipoDeChip ?? '').toString().trim(),
      planoContrato: (this.createModel?.planoContrato ?? '').toString().trim(),
      status: (this.createModel?.status ?? '').toString().trim(),
      contaEmpresa: (this.createModel?.contaEmpresa ?? '').toString().trim(),
      conta: (this.createModel?.conta ?? '').toString().trim(),
      dtEfetivacaoServico: (this.createModel?.dtEfetivacaoServico ?? '').toString().trim(),
      dtTerminoFidelizacao: (this.createModel?.dtTerminoFidelizacao ?? '').toString().trim()
    };
  }

  private createBatchDraftFromSource(
    source: any,
    seed?: Partial<CreateBatchLineDraft>,
    opts?: { keepLinha?: boolean; keepChip?: boolean; copyDetails?: boolean }
  ): CreateBatchLineDraft {
    const keepLinha = !!opts?.keepLinha;
    const keepChip = !!opts?.keepChip;
    const copyDetails = opts?.copyDetails ?? true;
    const baseSource = source ?? this.createModel ?? {};
    const raw = this.getBatchLineDataWithoutInternal(baseSource);

    this.createBatchUidSeed += 1;

    const row: CreateBatchLineDraft = {
      ...raw,
      ...(seed ? this.getBatchLineDataWithoutInternal(seed) : {}),
      uid: this.createBatchUidSeed,
      item: 0,
      linha: keepLinha ? (seed?.linha ?? raw.linha ?? '') : (seed?.linha ?? ''),
      chip: keepChip ? (seed?.chip ?? raw.chip ?? '') : (seed?.chip ?? ''),
      usuario: (seed?.usuario ?? raw.usuario ?? '').toString(),
      tipoDeChip: (seed?.tipoDeChip ?? raw.tipoDeChip ?? '').toString()
    };

    if (!copyDetails) {
      const clearScalarFields = [
        'contaEmpresa',
        'conta',
        'status',
        'planoContrato',
        'vencConta',
        'modalidade',
        'cedente',
        'solicitante',
        'dataBloqueio',
        'dataEntregaOpera',
        'dataEntregaCliente',
        'dtEfetivacaoServico',
        'dtTerminoFidelizacao'
      ];
      const clearNumericFields = [
        'franquiaVivo',
        'valorPlanoVivo',
        'gestaoVozDados',
        'skeelo',
        'vivoNewsPlus',
        'vivoTravelMundo',
        'vivoGestaoDispositivo',
        'vivoSync',
        'valorContratoVivo',
        'franquiaLine',
        'franquiaGestao',
        'locacaoAp',
        'valorContratoLine',
        'desconto',
        'lucro'
      ];

      const mutableRow = row as Record<string, any>;
      clearScalarFields.forEach((key) => (mutableRow[key] = ''));
      clearNumericFields.forEach((key) => (mutableRow[key] = null));

      if (seed) {
        Object.assign(row, this.getBatchLineDataWithoutInternal(seed));
      }
    }

    this.ensureBatchLineDefaults(row);
    return row;
  }

  private getBatchLineDataWithoutInternal(source: any): any {
    if (!source) return {};
    const { uid, ...rest } = source;
    return { ...rest };
  }

  private ensureBatchLineDefaults(row: any) {
    if (!row) return;

    if (!('item' in row)) row.item = 0;
    if (!('skil' in row) || row.skil === null || row.skil === undefined || row.skil === '') {
      row.skil = this.createMode === 'NEW_LINE_IN_GROUP' ? (this.createModel?.skil ?? 'PESSOA FÍSICA') : (this.createModel?.skil ?? 'PESSOA FÍSICA');
    }

    const scalarDefaults: Array<[string, any]> = [
      ['linha', ''],
      ['chip', ''],
      ['tipoDeChip', ''],
      ['usuario', ''],
      ['contaEmpresa', ''],
      ['conta', ''],
      ['status', ''],
      ['planoContrato', ''],
      ['vencConta', ''],
      ['modalidade', ''],
      ['cedente', ''],
      ['solicitante', ''],
      ['dataBloqueio', ''],
      ['dataEntregaOpera', ''],
      ['dataEntregaCliente', ''],
      ['dtEfetivacaoServico', ''],
      ['dtTerminoFidelizacao', '']
    ];

    const numericDefaults: Array<[string, any]> = [
      ['franquiaVivo', null],
      ['valorPlanoVivo', null],
      ['gestaoVozDados', null],
      ['skeelo', null],
      ['vivoNewsPlus', null],
      ['vivoTravelMundo', null],
      ['vivoGestaoDispositivo', null],
      ['vivoSync', null],
      ['valorContratoVivo', null],
      ['franquiaLine', null],
      ['franquiaGestao', null],
      ['locacaoAp', null],
      ['valorContratoLine', null],
      ['desconto', null],
      ['lucro', null]
    ];

    scalarDefaults.forEach(([k, v]) => {
      if (!(k in row) || row[k] === null || row[k] === undefined) row[k] = v;
    });
    numericDefaults.forEach(([k, v]) => {
      if (!(k in row)) row[k] = v;
    });

    if (!(row.contaEmpresa ?? '').toString().trim() && (row.conta ?? '').toString().trim()) {
      row.contaEmpresa = this.findEmpresaByConta(row.conta);
    }

    row.cliente = (this.createModel?.cliente ?? row.cliente ?? '').toString();
  }

  private recomputeBatchValidation() {
    const byUid: Record<number, BatchLineValidation> = {};
    const linhaCounts = new Map<string, number>();
    const chipCounts = new Map<string, number>();

    this.createBatchLines.forEach((row) => {
      const linhaDigits = (row?.linha ?? '').toString().replace(/\D/g, '');
      if (!linhaDigits) return;
      linhaCounts.set(linhaDigits, (linhaCounts.get(linhaDigits) ?? 0) + 1);
    });

    this.createBatchLines.forEach((row) => {
      const chipDigits = (row?.chip ?? '').toString().replace(/\D/g, '');
      if (!chipDigits) return;
      chipCounts.set(chipDigits, (chipCounts.get(chipDigits) ?? 0) + 1);
    });

    let valid = 0;
    let invalid = 0;
    let duplicates = 0;

    this.createBatchLines.forEach((row, index) => {
      const linhaRaw = (row?.linha ?? '').toString().trim();
      const chipRaw = (row?.chip ?? '').toString().trim();
      const linhaDigits = linhaRaw.replace(/\D/g, '');
      const chipDigits = chipRaw.replace(/\D/g, '');
      const errors: string[] = [];

      if (!linhaRaw) errors.push('Linha obrigatória.');
      else if (!linhaDigits) errors.push('Número de linha inválido.');

      if (!chipRaw) errors.push('Chip (ICCID) obrigatório.');
      else if (!chipDigits) errors.push('Chip (ICCID) inválido.');

      const contaEmpresa = (row?.['contaEmpresa'] ?? '').toString().trim();
      const conta = (row?.['conta'] ?? '').toString().trim();
      const status = (row?.['status'] ?? '').toString().trim();
      const plano = (row?.['planoContrato'] ?? '').toString().trim();
      const dtEfet = (row?.['dtEfetivacaoServico'] ?? '').toString().trim();
      const dtFidel = (row?.['dtTerminoFidelizacao'] ?? '').toString().trim();

      if (!contaEmpresa) errors.push('Empresa (Conta) obrigatória.');
      if (!conta) errors.push('Conta obrigatória.');
      if (!status) errors.push('Status obrigatório.');
      if (!plano) errors.push('Plano Contrato obrigatório.');
      if (!dtEfet) errors.push('Dt. Efetivação Serviço obrigatória.');
      if (!dtFidel) errors.push('Dt. Término Fidelização obrigatória.');

      const isLinhaDuplicate = !!linhaDigits && (linhaCounts.get(linhaDigits) ?? 0) > 1;
      const isChipDuplicate = !!chipDigits && (chipCounts.get(chipDigits) ?? 0) > 1;
      if (isLinhaDuplicate) {
        errors.push('Linha duplicada no lote.');
      }
      if (isChipDuplicate) {
        errors.push('Chip (ICCID) duplicado no lote.');
      }
      if (isLinhaDuplicate || isChipDuplicate) {
        duplicates++;
      }

      const hasDetailPending = errors.some((e) =>
        ['empresa', 'conta', 'status', 'plano', 'efetivação', 'fidelização'].some((k) =>
          e.toLowerCase().includes(k)
        )
      );
      if (hasDetailPending && !errors.some((e) => e.toLowerCase().includes('pendências nos detalhes'))) {
        errors.unshift('Pendências nos detalhes da linha.');
      }

      if (errors.length > 0) invalid++;
      else valid++;

      byUid[row.uid] = {
        uid: row.uid,
        index,
        linhaDigits,
        errors
      };
    });

    this.createBatchValidationByUid = byUid;
    this.createBatchValidationSummary = {
      total: this.createBatchLines.length,
      valid,
      invalid,
      duplicates
    };
  }

  private validateCreateClientFields(): string | null {
    if (this.createMode !== 'NEW_CLIENT') return null;

    if (!this.createModel.cliente) {
      return this.createModel.docType === 'PF' ? 'Informe o Nome Completo.' : 'Informe a Razão Social.';
    }
    if (!this.createModel.docNumber) {
      return `O ${this.createModel.docType === 'PF' ? 'CPF' : 'CNPJ'} é obrigatório.`;
    }
    return null;
  }

  private validateCreateCommonFields(opts?: { requireLinha?: boolean; requireChip?: boolean }): string | null {
    const requireLinha = opts?.requireLinha ?? true;
    const requireChip = opts?.requireChip ?? true;

    if (!this.createModel.contaEmpresa) return 'Selecione a Empresa (Conta).';
    if (!this.createModel.conta) return 'Selecione uma Conta.';
    if (requireLinha && !this.createModel.linha) return 'O número da Linha é obrigatório.';
    if (requireChip && !this.createModel.chip) return 'O Chip (ICCID) é obrigatório.';
    if (!this.createModel.status) return 'Selecione um Status.';
    if (!this.createModel.planoContrato) return 'Selecione um Plano.';
    if (!this.createModel.dtEfetivacaoServico) return 'A Dt. Efetivação Serviço é obrigatória.';
    if (!this.createModel.dtTerminoFidelizacao) return 'A Dt. Término Fidelização é obrigatória.';
    return null;
  }

  private validateBatchLines(): string | null {
    this.recomputeBatchValidation();

    if (this.createBatchLines.length === 0) {
      return 'Adicione ao menos uma linha no lote.';
    }

    if (this.createBatchValidationSummary.invalid <= 0) {
      return null;
    }

    for (let i = 0; i < this.createBatchLines.length; i++) {
      const row = this.createBatchLines[i];
      const errors = this.getBatchLineErrors(row.uid);
      if (errors.length <= 0) continue;
      return `Linha ${i + 1}: ${errors[0]}`;
    }

    return 'Existem linhas inválidas no lote.';
  }

  private buildCreatePayload(model: any): CreateMobileLineRequest {
    this.calculateFinancials(model);

    const { contaEmpresa: _contaEmpresa, uid: _uid, reservaLineId: _reservaLineId, ...createModelPayload } = model;

    return {
      ...createModelPayload,
      item: Number(model.item),
      dataBloqueio: this.dateInputToIso(model.dataBloqueio),
      dataEntregaOpera: this.dateInputToIso(model.dataEntregaOpera),
      dataEntregaCliente: this.dateInputToIso(model.dataEntregaCliente),
      dtEfetivacaoServico: this.dateInputToIso(model.dtEfetivacaoServico),
      dtTerminoFidelizacao: this.dateInputToIso(model.dtTerminoFidelizacao),
      franquiaVivo: this.toNullableNumber(model.franquiaVivo),
      valorPlanoVivo: this.toNullableNumber(model.valorPlanoVivo),
      gestaoVozDados: this.toNullableNumber(model.gestaoVozDados),
      skeelo: this.toNullableNumber(model.skeelo),
      vivoNewsPlus: this.toNullableNumber(model.vivoNewsPlus),
      vivoTravelMundo: this.toNullableNumber(model.vivoTravelMundo),
      vivoGestaoDispositivo: this.toNullableNumber(model.vivoGestaoDispositivo),
      vivoSync: this.toNullableNumber(model.vivoSync),
      valorContratoVivo: this.toNullableNumber(model.valorContratoVivo),
      franquiaLine: this.toNullableNumber(model.franquiaLine),
      franquiaGestao: this.toNullableNumber(model.franquiaGestao),
      locacaoAp: this.toNullableNumber(model.locacaoAp),
      valorContratoLine: this.toNullableNumber(model.valorContratoLine),
      desconto: this.toNullableNumber(model.desconto),
      lucro: this.toNullableNumber(model.lucro),
      tipoDeChip: (model.tipoDeChip ?? '').toString()
    };
  }

  private buildBatchPayloads(): CreateMobileLineRequest[] {
    const clientName = (this.createModel?.cliente ?? '').toString().trim();

    return this.createBatchLines.map((row) => {
      const lineModel = {
        ...row,
        cliente: clientName || (row?.['cliente'] ?? '').toString(),
        linha: (row.linha ?? '').toString(),
        chip: (row.chip ?? '').toString(),
        usuario: (row.usuario ?? '').toString(),
        tipoDeChip: (row.tipoDeChip ?? '').toString()
      };

      return this.buildCreatePayload(lineModel);
    });
  }

  private getCreateSuccessMessage(createdCount: number): string {
    if (createdCount <= 1) {
      return this.createMode === 'NEW_CLIENT' ? 'Sucesso! Cliente cadastrado.' : 'Linha cadastrada com sucesso.';
    }

    return `Sucesso! ${createdCount} linhas cadastradas no lote.`;
  }

  private async finalizeCreateSuccess(createdCount: number) {
    const targetClient = (this.createModel?.cliente ?? '').toString().trim();

    this.createSaving = false;
    this.closeAllModals();

    await this.showToast(this.getCreateSuccessMessage(createdCount));

    if (this.createMode === 'NEW_LINE_IN_GROUP' && this.expandedGroup === targetClient) {
      const term = (this.searchTerm ?? '').trim();
      const useTerm = term && this.isSpecificSearchTerm(term) ? term : undefined;

      this.fetchGroupLines(this.expandedGroup!, useTerm);
      this.loadGroups();
      this.loadKpis();
    } else {
      this.refreshData();
    }
  }

  private async handleCreateError(err: HttpErrorResponse, fallbackMessage = 'Erro ao criar registro.') {
    this.createSaving = false;
    const msg = (err.error as any)?.message || fallbackMessage;
    await this.showToast(msg);
  }

  onContaEmpresaChange(isEdit: boolean) {
    const model = isEdit ? this.editModel : this.createModel;
    if (!model) return;

    const contas = this.getContasByEmpresa(model.contaEmpresa);
    const selectedConta = (model.conta ?? '').toString().trim();

    if (!selectedConta) return;

    const hasMatch = contas.some((c) => this.sameConta(c, selectedConta));
    if (!hasMatch) model.conta = '';
  }

  onDocTypeChange() {
    this.createModel.docNumber = '';
    this.createModel.skil = this.createModel.docType === 'PF' ? 'PESSOA FÍSICA' : 'PESSOA JURÍDICA';
  }

  onDocInput(event: any) {
    let value = event.target.value.replace(/\D/g, '');
    if (this.createModel.docType === 'PF') {
      if (value.length > 11) value = value.slice(0, 11);
      value = value.replace(/(\d{3})(\d)/, '$1.$2');
      value = value.replace(/(\d{3})(\d)/, '$1.$2');
      value = value.replace(/(\d{3})(\d{1,2})$/, '$1-$2');
    } else {
      if (value.length > 14) value = value.slice(0, 14);
      value = value.replace(/^(\d{2})(\d)/, '$1.$2');
      value = value.replace(/^(\d{2})\.(\d{3})(\d)/, '$1.$2.$3');
      value = value.replace(/\.(\d{3})(\d)/, '.$1/$2');
      value = value.replace(/(\d{4})(\d)/, '$1-$2');
    }
    this.createModel.docNumber = value;
  }

  private async loadCreateReservaLines(): Promise<void> {
    if (this.loadingCreateReservaLines) return;
    this.loadingCreateReservaLines = true;

    try {
      const pageSize = 500;
      let page = 1;
      const collected: ReservaLineOption[] = [];

      while (true) {
        const params = new HttpParams()
          .set('page', String(page))
          .set('pageSize', String(pageSize))
          .set('skil', 'RESERVA');

        const response = await firstValueFrom(this.http.get<ApiPagedResult<ApiLineList>>(this.apiBase, { params }));
        const items = Array.isArray(response?.items) ? response.items : [];

        for (const row of items) {
          const id = (row?.id ?? '').toString().trim();
          const linha = (row?.linha ?? '').toString().trim();
          if (!id || !linha) continue;
          collected.push({
            value: id,
            label: `${row?.item ?? ''} • ${linha} • ${(row?.usuario ?? 'SEM USUÁRIO').toString()}`,
            linha,
            chip: (row?.chip ?? '').toString(),
            usuario: (row?.usuario ?? '').toString()
          });
        }

        const total = Number(response?.total ?? 0);
        if (!items.length || (total > 0 && collected.length >= total) || items.length < pageSize) break;
        page += 1;
      }

      const seen = new Set<string>();
      const unique = collected.filter((opt) => {
        if (seen.has(opt.value)) return false;
        seen.add(opt.value);
        return true;
      });

      unique.sort((a, b) => a.linha.localeCompare(b.linha, 'pt-BR', { numeric: true, sensitivity: 'base' }));

      this.createReservaLineOptions = unique;
      this.createReservaLineLookup = new Map(unique.map((opt) => [opt.value, opt]));
    } catch {
      this.createReservaLineOptions = [];
      this.createReservaLineLookup.clear();
      await this.showToast('Erro ao carregar linhas da Reserva.');
    } finally {
      this.loadingCreateReservaLines = false;
    }
  }

  onCreateReservaLineChange() {
    const lineId = (this.createModel?.reservaLineId ?? '').toString().trim();
    if (!lineId) {
      this.createModel.linha = '';
      return;
    }

    const selected = this.createReservaLineLookup.get(lineId);
    if (selected) {
      this.createModel.linha = selected.linha ?? '';
      if (!String(this.createModel.chip ?? '').trim() && selected.chip) {
        this.createModel.chip = selected.chip;
      }
      if (!String(this.createModel.usuario ?? '').trim() && selected.usuario) {
        this.createModel.usuario = selected.usuario;
      }
    }

    this.http.get<ApiLineDetail>(`${this.apiBase}/${lineId}`).subscribe({
      next: (detail) => {
        this.createModel.linha = (detail?.linha ?? this.createModel.linha ?? '').toString();
        if (!String(this.createModel.chip ?? '').trim() && detail?.chip) {
          this.createModel.chip = detail.chip;
        }
        if (!String(this.createModel.tipoDeChip ?? '').trim() && detail?.tipoDeChip) {
          this.createModel.tipoDeChip = detail.tipoDeChip;
        }
        if (!String(this.createModel.usuario ?? '').trim() && detail?.usuario) {
          this.createModel.usuario = detail.usuario;
        }
      },
      error: () => {
        // Mantém dados já carregados da lista.
      }
    });
  }

  async saveCreate() {
    if (this.isCreateBatchMode) {
      await this.saveCreateBatch();
      return;
    }

    await this.saveCreateSingle();
  }

  private async saveCreateSingle() {
    const clientError = this.validateCreateClientFields();
    if (clientError) {
      await this.showToast(clientError);
      return;
    }

    const commonError = this.validateCreateCommonFields({ requireLinha: true, requireChip: true });
    if (commonError) {
      await this.showToast(commonError);
      return;
    }

    this.createSaving = true;
    const payload = this.buildCreatePayload(this.createModel);

    this.http.post<ApiLineDetail>(this.apiBase, payload).subscribe({
      next: async () => {
        await this.finalizeCreateSuccess(1);
      },
      error: async (err: HttpErrorResponse) => {
        await this.handleCreateError(err);
      }
    });
  }

  isReservaLineSelected(id: string): boolean {
    return this.reservaSelectedLineIds.includes(id);
  }

  toggleReservaLineSelection(id: string, checked?: boolean) {
    if (!id || !this.hasGroupLineSelectionTools) return;
    const exists = this.reservaSelectedLineIds.includes(id);
    const shouldSelect = typeof checked === 'boolean' ? checked : !exists;
    if (shouldSelect && !exists) {
      this.reservaSelectedLineIds = [...this.reservaSelectedLineIds, id];
      return;
    }
    if (!shouldSelect && exists) {
      this.reservaSelectedLineIds = this.reservaSelectedLineIds.filter((x) => x !== id);
    }
  }

  toggleSelectAllReservaGroupLines() {
    if (!this.hasGroupLineSelectionTools) return;
    const ids = (this.groupLines ?? []).map((x) => x.id).filter(Boolean);
    if (ids.length === 0) {
      this.reservaSelectedLineIds = [];
      return;
    }
    if (this.reservaSelectedLineIds.length === ids.length && ids.every((id) => this.reservaSelectedLineIds.includes(id))) {
      this.reservaSelectedLineIds = [];
      return;
    }
    this.reservaSelectedLineIds = [...ids];
  }

  clearReservaSelection() {
    if (this.reservaSelectedLineIds.length === 0) return;
    this.reservaSelectedLineIds = [];
  }

  async openReservaTransferModal() {
    if (!this.isReservaExpandedGroup) {
      await this.showToast('Abra um grupo no filtro Reserva para selecionar e atribuir linhas.');
      return;
    }
    if (this.reservaSelectedCount <= 0) {
      await this.showToast('Selecione ao menos uma linha da Reserva.');
      return;
    }

    this.reservaTransferOpen = true;
    this.reservaTransferSaving = false;
    this.reservaTransferLastResult = null;
    this.reservaTransferModel = {
      clienteDestino: '',
      usuarioDestino: '',
      skilDestino: ''
    };
    this.cdr.detectChanges();

    this.loadReservaTransferClients();
  }

  async openMoveToReservaModal() {
    if (!this.hasGroupLineSelectionTools || !this.expandedGroup) {
      await this.showToast('Abra um grupo para selecionar linhas.');
      return;
    }

    if (this.isReservaExpandedGroup || this.isExpandedGroupNamedReserva) {
      await this.showToast('Esse grupo já está no contexto da Reserva.');
      return;
    }

    if (this.reservaSelectedCount <= 0) {
      await this.showToast('Selecione ao menos uma linha para enviar à Reserva.');
      return;
    }

    this.moveToReservaOpen = true;
    this.moveToReservaSaving = false;
    this.moveToReservaLastResult = null;
    this.cdr.detectChanges();
  }

  private loadReservaTransferClients() {
    this.http.get<string[]>(`${this.apiBase}/clients`).subscribe({
      next: (clients) => {
        this.reservaTransferClients = (clients ?? []).filter((x) => !!(x ?? '').toString().trim());
        this.cdr.detectChanges();
      },
      error: () => {
        this.reservaTransferClients = [];
      }
    });
  }

  async submitReservaTransfer() {
    if (this.reservaTransferSaving) return;

    const clienteDestino = (this.reservaTransferModel.clienteDestino ?? '').toString().trim();
    if (!clienteDestino) {
      await this.showToast('Informe o cliente de destino.');
      return;
    }

    if (clienteDestino.localeCompare('RESERVA', 'pt-BR', { sensitivity: 'base' }) === 0) {
      await this.showToast('O cliente de destino não pode ser RESERVA.');
      return;
    }

    if (this.reservaSelectedCount <= 0) {
      await this.showToast('Selecione ao menos uma linha da Reserva.');
      return;
    }

    const payload: AssignReservaLinesRequestDto = {
      clienteDestino,
      usuarioDestino: (this.reservaTransferModel.usuarioDestino ?? '').toString().trim() || null,
      skilDestino: (this.reservaTransferModel.skilDestino ?? '').toString().trim() || null,
      lineIds: [...this.reservaSelectedLineIds]
    };

    this.reservaTransferSaving = true;

    this.http.post<AssignReservaLinesResultDto>(`${this.apiBase}/reserva/assign-client`, payload).subscribe({
      next: async (res) => {
        this.reservaTransferSaving = false;
        this.reservaTransferLastResult = res;

        const ok = Number(res?.updated ?? 0) || 0;
        const failed = Number(res?.failed ?? 0) || 0;

        if (ok > 0) {
          this.clearReservaSelection();
          this.reservaTransferOpen = false;

          await this.showToast(
            failed > 0
              ? `Transferência concluída com pendências: ${ok} linha(s) atribuída(s), ${failed} falha(s).`
              : `${ok} linha(s) da Reserva atribuída(s) com sucesso.`
          );

          if (this.expandedGroup) {
            const term = (this.searchTerm ?? '').trim();
            const useTerm = term && this.isSpecificSearchTerm(term) ? term : undefined;
            this.fetchGroupLines(this.expandedGroup, useTerm);
          }
          this.loadGroups();
          this.loadKpis();
          return;
        }

        const firstError = res?.items?.find((x) => !x.success)?.message || 'Nenhuma linha foi atribuída.';
        await this.showToast(firstError);
      },
      error: async (err: HttpErrorResponse) => {
        this.reservaTransferSaving = false;
        const msg = (err.error as any)?.message || 'Erro ao atribuir linhas da Reserva.';
        await this.showToast(msg);
      }
    });
  }

  async submitMoveToReserva() {
    if (this.moveToReservaSaving) return;
    if (!this.canMoveSelectedLinesToReserva) {
      await this.showToast('Selecione linhas de um cliente para enviar à Reserva.');
      return;
    }
    if (this.reservaSelectedCount <= 0) {
      await this.showToast('Selecione ao menos uma linha para enviar à Reserva.');
      return;
    }

    const payload: MoveLinesToReservaRequestDto = {
      lineIds: [...this.reservaSelectedLineIds]
    };

    this.moveToReservaSaving = true;

    this.http.post<AssignReservaLinesResultDto>(`${this.apiBase}/move-to-reserva`, payload).subscribe({
      next: async (res) => {
        this.moveToReservaSaving = false;
        this.moveToReservaLastResult = res;

        const ok = Number(res?.updated ?? 0) || 0;
        const failed = Number(res?.failed ?? 0) || 0;

        if (ok > 0) {
          this.clearReservaSelection();
          this.moveToReservaOpen = false;

          await this.showToast(
            failed > 0
              ? `Envio para Reserva concluído com pendências: ${ok} linha(s) enviada(s), ${failed} falha(s).`
              : `${ok} linha(s) enviada(s) para a Reserva com sucesso.`
          );

          if (this.expandedGroup) {
            const term = (this.searchTerm ?? '').trim();
            const useTerm = term && this.isSpecificSearchTerm(term) ? term : undefined;
            this.fetchGroupLines(this.expandedGroup, useTerm);
          }
          this.loadGroups();
          this.loadKpis();
          return;
        }

        const firstError = res?.items?.find((x) => !x.success)?.message || 'Nenhuma linha foi enviada para a Reserva.';
        await this.showToast(firstError);
      },
      error: async (err: HttpErrorResponse) => {
        this.moveToReservaSaving = false;
        const msg = (err.error as any)?.message || 'Erro ao enviar linhas para a Reserva.';
        await this.showToast(msg);
      }
    });
  }

  async onDownloadBatchExcelTemplate() {
    if (this.batchExcelTemplateDownloading) return;

    this.batchExcelTemplateDownloading = true;
    const params = new HttpParams().set('_', `${Date.now()}`);
    this.http.get(`${this.templatesApiBase}/planilha-geral`, { params, observe: 'response', responseType: 'blob' }).subscribe({
      next: async (res) => {
        this.batchExcelTemplateDownloading = false;

        const blob = res.body;
        if (!blob) {
          await this.showToast('Não foi possível baixar o modelo da planilha.');
          return;
        }

        const disposition = res.headers.get('content-disposition') || '';
        const fileName = this.extractDownloadFileName(disposition) || 'MODELO_GERAL_LINEGESTAO.xlsx';

        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = fileName;
        document.body.appendChild(a);
        a.click();
        a.remove();
        setTimeout(() => URL.revokeObjectURL(url), 0);
      },
      error: async (err: HttpErrorResponse) => {
        this.batchExcelTemplateDownloading = false;
        const msg = (err.error as any)?.message || 'Erro ao baixar o modelo da planilha.';
        await this.showToast(msg);
      }
    });
  }

  private extractDownloadFileName(contentDisposition: string): string | null {
    const raw = (contentDisposition ?? '').trim();
    if (!raw) return null;

    const utf8Match = raw.match(/filename\*\s*=\s*UTF-8''([^;]+)/i);
    if (utf8Match?.[1]) {
      try {
        return decodeURIComponent(utf8Match[1].trim().replace(/^"(.*)"$/, '$1'));
      } catch {
        return utf8Match[1].trim().replace(/^"(.*)"$/, '$1');
      }
    }

    const simpleMatch = raw.match(/filename\s*=\s*([^;]+)/i);
    if (!simpleMatch?.[1]) return null;
    return simpleMatch[1].trim().replace(/^"(.*)"$/, '$1');
  }

  async onImportBatchExcel() {
    if (this.createSaving) return;
    if (!this.isCreateBatchMode) {
      await this.showToast('Ative o modo Lote de Linhas para importar a planilha.');
      return;
    }

    if (!this.batchExcelInput?.nativeElement) return;
    this.batchExcelInput.nativeElement.value = '';
    this.batchExcelInput.nativeElement.click();
  }

  onBatchExcelSelected(ev: Event) {
    const file = (ev.target as HTMLInputElement).files?.[0];
    if (!file) return;

    const form = new FormData();
    form.append('file', file);

    this.batchExcelPreviewLoading = true;
    this.batchExcelPreview = null;

    this.http.post<BatchExcelPreviewResultDto>(`${this.apiBase}/batch/import-preview`, form).subscribe({
      next: (preview) => {
        this.batchExcelPreviewLoading = false;
        this.batchExcelPreview = preview;
        this.cdr.detectChanges();
      },
      error: async (err: HttpErrorResponse) => {
        this.batchExcelPreviewLoading = false;
        this.batchExcelPreview = null;
        const msg = (err.error as any)?.message || 'Falha ao pré-visualizar a planilha do lote.';
        await this.showToast(msg);
      }
    });
  }

  clearBatchExcelPreview() {
    this.batchExcelPreview = null;
    this.batchExcelPreviewLoading = false;
  }

  private mapBatchExcelPreviewRowToSeed(row: BatchExcelPreviewRowDto): Partial<CreateBatchLineDraft> {
    const data = row?.data ?? {};
    return {
      ...data,
      item: 0,
      linha: (data.linha ?? '').toString(),
      chip: (data.chip ?? '').toString(),
      usuario: (data.usuario ?? '').toString(),
      tipoDeChip: (data.tipoDeChip ?? '').toString(),
      dataBloqueio: this.isoToDateInput(data.dataBloqueio as any),
      dataEntregaOpera: this.isoToDateInput(data.dataEntregaOpera as any),
      dataEntregaCliente: this.isoToDateInput(data.dataEntregaCliente as any),
      dtEfetivacaoServico: this.isoToDateInput(data.dtEfetivacaoServico as any),
      dtTerminoFidelizacao: this.isoToDateInput(data.dtTerminoFidelizacao as any)
    };
  }

  async applyBatchExcelPreview(mode: BatchMassApplyMode) {
    const preview = this.batchExcelPreview;
    if (!preview) {
      await this.showToast('Importe uma planilha para gerar a pré-visualização.');
      return;
    }

    if ((preview.headerErrors?.length ?? 0) > 0) {
      await this.showToast(preview.headerErrors[0]?.message || 'Corrija os erros de cabeçalho antes de aplicar.');
      return;
    }

    const validRows = (preview.rows ?? []).filter((row) => row.valid);
    if (validRows.length <= 0) {
      await this.showToast('Nenhuma linha válida encontrada na planilha para carregar no lote.');
      return;
    }

    const parsedRows = validRows.map((row) =>
      this.createBatchDraftFromSource(
        this.createModel,
        this.mapBatchExcelPreviewRowToSeed(row),
        { keepLinha: true, keepChip: true, copyDetails: true }
      )
    );

    this.createBatchLines = mergeMassRows(this.createBatchLines, parsedRows, mode);
    this.selectedBatchLineUid = parsedRows[parsedRows.length - 1]?.uid ?? this.selectedBatchLineUid;
    this.batchDetailOpen = this.createBatchLines.length > 0;
    this.recomputeBatchValidation();

    await this.showToast(
      mode === 'REPLACE'
        ? `${parsedRows.length} linha(s) válida(s) carregada(s) da planilha (substituindo o lote atual).`
        : `${parsedRows.length} linha(s) válida(s) adicionada(s) ao lote pela planilha.`
    );
  }

  private async saveCreateBatch() {
    const clientError = this.validateCreateClientFields();
    if (clientError) {
      await this.showToast(clientError);
      return;
    }

    const batchError = this.validateBatchLines();
    if (batchError) {
      await this.showToast(batchError);
      return;
    }

    this.createSaving = true;

    const payload: CreateMobileLinesBatchRequest = {
      lines: this.buildBatchPayloads()
    };

    this.http.post<CreateMobileLinesBatchResponse>(`${this.apiBase}/batch`, payload).subscribe({
      next: async (res) => {
        const createdCount = Number(res?.created ?? payload.lines.length) || payload.lines.length;
        await this.finalizeCreateSuccess(createdCount);
      },
      error: async (err: HttpErrorResponse) => {
        if (err.status === 405) {
          await this.showToast(
            'A API em execução não aceita POST em /api/lines/batch (405). Reinicie/atualize o backend para a versão com o endpoint de lote.'
          );
          this.createSaving = false;
          return;
        }

        await this.handleCreateError(err, 'Erro ao criar lote de linhas.');
      }
    });
  }

  private async showToast(message: string) {
    if (!isPlatformBrowser(this.platformId)) return;

    this.toastMessage = message;
    this.cdr.detectChanges();

    if (!this.successToast?.nativeElement) return;

    try {
      const bs = await import('bootstrap');
      const toastInstance = bs.Toast.getOrCreateInstance(this.successToast.nativeElement, {
        autohide: true,
        delay: 3000
      });
      toastInstance.show();
    } catch (error) {
      console.error(error);
    }
  }

  formatMoney(v: any): string {
    if (v == null || Number.isNaN(v)) return '-';
    return new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(v);
  }

  formatNumber(v: any): string {
    if (v == null || Number.isNaN(v)) return '-';
    return new Intl.NumberFormat('pt-BR').format(v);
  }

  formatFranquia(v: any): string {
    if (v == null || Number.isNaN(v)) return '-';
    return `${new Intl.NumberFormat('pt-BR').format(v)} GB`;
  }

  formatDateBr(iso: any): string {
    if (!iso) return '-';
    const d = new Date(iso);
    return Number.isNaN(d.getTime()) ? '-' : new Intl.DateTimeFormat('pt-BR').format(d);
  }

  statusClass(s: any): string {
    const n = (s ?? '').toString().toLowerCase();
    if (n.includes('bloq') || n.includes('perda') || n.includes('roubo')) return 'is-blocked';
    if (n.includes('ativo')) return 'is-active';
    return '';
  }

  statusLabel(s: any): string {
    const v = (s ?? '').toString().trim();
    return v || '-';
  }

  private toEditModel(d: ApiLineDetail): any {
    return {
      ...d,
      item: d.item ?? 0,
      dataBloqueio: this.isoToDateInput(d.dataBloqueio),
      dataEntregaOpera: this.isoToDateInput(d.dataEntregaOpera),
      dataEntregaCliente: this.isoToDateInput(d.dataEntregaCliente),
      dtEfetivacaoServico: this.isoToDateInput(d.dtEfetivacaoServico),
      dtTerminoFidelizacao: this.isoToDateInput(d.dtTerminoFidelizacao),

      franquiaVivo: d.franquiaVivo ?? null,
      valorPlanoVivo: d.valorPlanoVivo ?? null,
      gestaoVozDados: d.gestaoVozDados ?? null,
      skeelo: d.skeelo ?? null,
      vivoNewsPlus: d.vivoNewsPlus ?? null,
      vivoTravelMundo: d.vivoTravelMundo ?? null,
      vivoGestaoDispositivo: d.vivoGestaoDispositivo ?? null,
      vivoSync: d.vivoSync ?? null,
      valorContratoVivo: d.valorContratoVivo ?? null,

      franquiaLine: d.franquiaLine ?? null,
      franquiaGestao: d.franquiaGestao ?? null,
      locacaoAp: d.locacaoAp ?? null,
      valorContratoLine: d.valorContratoLine ?? null,

      desconto: d.desconto ?? null,
      lucro: d.lucro ?? null,
      tipoDeChip: d.tipoDeChip ?? '',
      contaEmpresa: this.findEmpresaByConta(d.conta)
    };
  }

  private isoToDateInput(iso?: string | null): string {
    if (!iso) return '';
    const s = iso.toString();
    // se já vier YYYY-MM-DD, mantém
    if (/^\d{4}-\d{2}-\d{2}$/.test(s)) return s;
    // se vier ISO completo, pega só a data
    if (s.includes('T')) return s.slice(0, 10);
    return s;
  }

  private dateInputToIso(v: any): string | null {
    const s = (v ?? '').toString().trim();
    if (!s) return null;
    // Mantém YYYY-MM-DD (excelente para DateOnly no backend)
    if (/^\d{4}-\d{2}-\d{2}$/.test(s)) return s;
    return s;
  }

  private toInt(v: any): number {
    if (v === null || v === undefined || v === '') return 0;
    const n = parseInt(v.toString(), 10);
    return Number.isNaN(n) ? 0 : n;
  }

  private toNullableNumber(v: any): number | null {
    if (v === null || v === undefined || v === '') return null;
    if (typeof v === 'number') return Number.isNaN(v) ? null : v;
    const n = parseFloat(v.toString().replace(',', '.'));
    return Number.isNaN(n) ? null : n;
  }

  private mergeOption(current: any, list: string[]): string[] {
    const v = (current ?? '').toString().trim();
    if (!v) return list;
    return list.includes(v) ? list : [v, ...list];
  }

  private mergeOptionList(base: string[], extra: string[]): string[] {
    const result: string[] = [...base];
    const seen = new Set(base.map((x) => x.trim()).filter(Boolean));

    extra.forEach((raw) => {
      const v = (raw ?? '').toString().trim();
      if (!v || seen.has(v)) return;
      seen.add(v);
      result.push(v);
    });

    return result;
  }

  private normalizeAccountCompanies(data: AccountCompanyOption[] | null | undefined): AccountCompanyOption[] {
    if (!Array.isArray(data)) return [];

    const result: AccountCompanyOption[] = [];

    data.forEach((item) => {
      const empresa = (item?.empresa ?? '').toString().trim();
      if (!empresa) return;

      const contas = this.mergeOptionList([], (item?.contas ?? []).map((x) => (x ?? '').toString().trim()));
      result.push({ empresa, contas });
    });

    return result;
  }

  private getAllContas(): string[] {
    const all = this.accountCompanies.flatMap((x) => x.contas ?? []);
    return this.mergeOptionList([], all);
  }

  private getContasByEmpresa(empresa: any): string[] {
    const target = (empresa ?? '').toString().trim();
    if (!target) return [];

    const found = this.accountCompanies.find((x) =>
      x.empresa.localeCompare(target, 'pt-BR', { sensitivity: 'base' }) === 0
    );
    return found ? [...found.contas] : [];
  }

  private findEmpresaByConta(conta: any): string {
    const target = this.normalizeConta(conta);
    if (!target) return '';

    const found = this.accountCompanies.find((group) =>
      (group.contas ?? []).some((c) => this.sameConta(c, target))
    );
    return found?.empresa ?? '';
  }

  private normalizeConta(value: any): string {
    const raw = (value ?? '').toString().trim();
    if (!raw) return '';
    if (!/^\d+$/.test(raw)) return raw.toUpperCase();
    const noLeadingZero = raw.replace(/^0+/, '');
    return noLeadingZero || '0';
  }

  private sameConta(a: any, b: any): boolean {
    return this.normalizeConta(a) === this.normalizeConta(b);
  }

  private syncContaEmpresaSelection(model: any) {
    if (!model) return;

    const empresaAtual = (model.contaEmpresa ?? '').toString().trim();
    if (empresaAtual) return;

    model.contaEmpresa = this.findEmpresaByConta(model.conta);
  }
}
