import { Injectable, computed, inject, signal } from '@angular/core';
import { catchError, of, take } from 'rxjs';
import { AdminCategory, AdminCategoryEditorMode, AdminCategoryListFilters } from '../models/admin-category.model';
import { AdminCategoriesFormFactory } from '../services/admin-categories-form.factory';
import { ADMIN_CATEGORIES_GATEWAY } from '../services/admin-categories-gateway.token';
import { LocalStorageService } from '../../../../core/storage/local-storage.service';
import { ProjectEditorFacade } from '../../../project-editor/facade/project-editor.facade';

const DRAFT_KEY_PREFIX = 'admin-category-draft:';
const NEW_CATEGORY_DRAFT_KEY = `${DRAFT_KEY_PREFIX}new`;
const VIEW_MODE_KEY = 'admin-categories:view-mode';
const DENSITY_KEY = 'admin-categories:density';
const COLUMNS_KEY = 'admin-categories:visible-columns';
const EXPANDED_KEY = 'admin-categories:expanded';

export type AdminCategoriesViewMode = 'tree' | 'table' | 'cards';
export type AdminCategoriesDensity = 'comfortable' | 'compact';

export const ALL_CATEGORY_COLUMNS = ['slug', 'items', 'status', 'visibility', 'updated'] as const;
export type AdminCategoryColumn = typeof ALL_CATEGORY_COLUMNS[number];

export interface AdminCategoryHealth {
  hasImage: boolean;
  hasSeo: boolean;
  hasDescription: boolean;
  hasValidParent: boolean;
  isVisible: boolean;
  hasProducts: boolean;
  completionPercent: number;
}

export interface AdminCategoriesDashboardStats {
  total: number;
  visible: number;
  hidden: number;
  empty: number;
  rootCount: number;
  subCount: number;
  missingImages: number;
  missingSeo: number;
  lastModifiedAt: string | null;
  completionPercent: number;
  recommendation: { labelKey: string; categoryId?: string };
}

@Injectable({ providedIn: 'root' })
export class AdminCategoriesFacade {
  private readonly gateway = inject(ADMIN_CATEGORIES_GATEWAY);
  private readonly formFactory = inject(AdminCategoriesFormFactory);
  private readonly localStorage = inject(LocalStorageService);
  private readonly projectEditor = inject(ProjectEditorFacade);

  readonly supportedLocales = computed(() => this.projectEditor.bootstrap()?.localization.supportedLocales ?? ['en']);

  readonly viewMode = signal<AdminCategoriesViewMode>((this.localStorage.getItem(VIEW_MODE_KEY) as AdminCategoriesViewMode) || 'tree');
  readonly density = signal<AdminCategoriesDensity>((this.localStorage.getItem(DENSITY_KEY) as AdminCategoriesDensity) || 'comfortable');
  readonly visibleColumns = signal<AdminCategoryColumn[]>(this.localStorage.getJSON<AdminCategoryColumn[]>(COLUMNS_KEY) ?? [...ALL_CATEGORY_COLUMNS]);
  readonly expandedIds = signal<Set<string>>(new Set(this.localStorage.getJSON<string[]>(EXPANDED_KEY) ?? []));
  readonly selectedIds = signal<string[]>([]);

  readonly hasSelection = computed(() => this.selectedIds().length > 0);

  setViewMode(mode: AdminCategoriesViewMode): void {
    this.viewMode.set(mode);
    this.localStorage.setItem(VIEW_MODE_KEY, mode);
  }

  setDensity(density: AdminCategoriesDensity): void {
    this.density.set(density);
    this.localStorage.setItem(DENSITY_KEY, density);
  }

  setColumnVisible(column: AdminCategoryColumn, visible: boolean): void {
    const next = visible ? [...new Set([...this.visibleColumns(), column])] : this.visibleColumns().filter(c => c !== column);
    this.visibleColumns.set(next);
    this.localStorage.setJSON(COLUMNS_KEY, next);
  }

  isExpanded(id: string): boolean {
    return this.expandedIds().has(id);
  }

  toggleExpanded(id: string): void {
    const next = new Set(this.expandedIds());
    next.has(id) ? next.delete(id) : next.add(id);
    this.expandedIds.set(next);
    this.localStorage.setJSON(EXPANDED_KEY, [...next]);
  }

  /** Expands every ancestor of `id` (not `id` itself) so a deep node becomes reachable - used by search and "jump to parent". */
  expandAncestorsOf(id: string): void {
    const next = new Set(this.expandedIds());
    let current = this.categories().find(category => category.id === id);
    while (current?.parentId) {
      next.add(current.parentId);
      current = this.categories().find(category => category.id === current!.parentId);
    }
    this.expandedIds.set(next);
    this.localStorage.setJSON(EXPANDED_KEY, [...next]);
  }

  expandAll(): void {
    const next = new Set(this.categories().map(category => category.id));
    this.expandedIds.set(next);
    this.localStorage.setJSON(EXPANDED_KEY, [...next]);
  }

  collapseAll(): void {
    this.expandedIds.set(new Set());
    this.localStorage.setJSON(EXPANDED_KEY, []);
  }

  toggleSelection(id: string, checked: boolean): void {
    this.selectedIds.update(current => checked ? [...new Set([...current, id])] : current.filter(item => item !== id));
  }

  toggleAll(checked: boolean): void {
    this.selectedIds.set(checked ? this.categories().map(category => category.id) : []);
  }

  clearSelection(): void {
    this.selectedIds.set([]);
  }

  ensureLocalesLoaded(): void {
    if (!this.projectEditor.bootstrap()) {
      this.projectEditor.loadBootstrap();
    }
  }

  readonly filters = signal<AdminCategoryListFilters>({ search: '', visibility: 'all', includeDeleted: false });
  readonly categories = signal<AdminCategory[]>([]);
  readonly loading = signal(false);
  readonly error = signal<string | null>(null);
  readonly draft = signal<AdminCategory | null>(null);
  readonly editorMode = signal<AdminCategoryEditorMode>('create');
  readonly dirty = signal(false);
  readonly slugTaken = signal(false);
  readonly slugCheckError = signal(false);
  readonly mutationError = signal<string | null>(null);
  private savedSnapshot: string | null = null;
  private draftStorageKey: string | null = null;

  readonly rootCategories = computed(() => this.categories().filter(category => !category.parentId));
  readonly childrenByParent = computed(() => {
    const map = new Map<string, AdminCategory[]>();
    for (const category of this.categories()) {
      if (!category.parentId) continue;
      const list = map.get(category.parentId) ?? [];
      list.push(category);
      map.set(category.parentId, list);
    }
    return map;
  });

  childrenOf(id: string): AdminCategory[] {
    return this.childrenByParent().get(id) ?? [];
  }

  breadcrumbFor(id: string | null): string[] {
    const trail: string[] = [];
    let current = this.categories().find(category => category.id === id);
    while (current) {
      trail.unshift(current.title);
      current = current.parentId ? this.categories().find(category => category.id === current!.parentId) : undefined;
    }
    return trail;
  }

  /**
   * Always fetches the full unfiltered catalog (search/visibility/deleted always 'all'
   * at the gateway) so parent/child chains never go missing when a filter would have
   * excluded an ancestor - the tree previously broke this way. All filtering for both
   * the tree and the flat table/cards views happens client-side in filteredCategories()
   * and visibleTreeRows() below.
   */
  loadList(): void {
    this.loading.set(true);
    this.error.set(null);
    this.gateway.loadCategories({ search: '', visibility: 'all', includeDeleted: true }).pipe(take(1)).subscribe({
      next: categories => {
        this.categories.set(categories);
        this.loading.set(false);
      },
      error: () => {
        this.categories.set([]);
        this.loading.set(false);
        this.error.set('common.errorDescription');
      }
    });
  }

  updateFilters(patch: Partial<AdminCategoryListFilters>): void {
    this.filters.update(current => ({ ...current, ...patch }));
  }

  private matchesSearch(category: AdminCategory, query: string): boolean {
    return !query || category.title.toLowerCase().includes(query.trim().toLowerCase());
  }

  /** Flat view (table/cards) - applies search/visibility/includeDeleted client-side. */
  readonly filteredCategories = computed(() => {
    const { search, visibility, includeDeleted } = this.filters();
    return this.categories()
      .filter(category => includeDeleted || !category.deletedAt)
      .filter(category => visibility === 'all' || (visibility === 'visible' ? category.visible : !category.visible))
      .filter(category => this.matchesSearch(category, search))
      .sort((a, b) => a.order - b.order);
  });

  /**
   * Tree rows, respecting expand/collapse state - and while searching, branches with no
   * matching descendant are hidden and matching branches are force-expanded (search always
   * wins over manually-collapsed state), reverting to the remembered expand state once the
   * search is cleared.
   */
  readonly visibleTreeRows = computed<{ category: AdminCategory; depth: number; hasChildren: boolean }[]>(() => {
    const { search, visibility, includeDeleted } = this.filters();
    const visible = (category: AdminCategory) => (includeDeleted || !category.deletedAt) && (visibility === 'all' || (visibility === 'visible' ? category.visible : !category.visible));
    const query = search.trim();

    const keepSet = query ? this.computeSearchKeepSet(query, visible) : null;
    const expanded = this.expandedIds();

    const build = (parentId: string | null, depth: number): { category: AdminCategory; depth: number; hasChildren: boolean }[] => {
      const siblings = (parentId === null ? this.categories().filter(c => !c.parentId) : this.categories().filter(c => c.parentId === parentId))
        .filter(visible)
        .filter(category => !keepSet || keepSet.has(category.id))
        .sort((a, b) => a.order - b.order);

      return siblings.flatMap(category => {
        const children = this.categories().filter(c => c.parentId === category.id).filter(visible);
        const hasChildren = children.length > 0;
        const isOpen = query ? true : expanded.has(category.id);
        const row = { category, depth, hasChildren };
        const nested = hasChildren && isOpen ? build(category.id, depth + 1) : [];
        return [row, ...nested];
      });
    };

    return build(null, 0);
  });

  private computeSearchKeepSet(query: string, visible: (category: AdminCategory) => boolean): Set<string> {
    const all = this.categories().filter(visible);
    const matches = all.filter(category => this.matchesSearch(category, query));
    const keep = new Set<string>();

    const addAncestors = (id: string) => {
      let current = all.find(category => category.id === id);
      while (current) {
        keep.add(current.id);
        current = current.parentId ? all.find(category => category.id === current!.parentId) : undefined;
      }
    };
    const addDescendants = (id: string) => {
      keep.add(id);
      all.filter(category => category.parentId === id).forEach(child => addDescendants(child.id));
    };

    matches.forEach(match => {
      addAncestors(match.id);
      addDescendants(match.id);
    });
    return keep;
  }

  startCreate(): void {
    this.editorMode.set('create');
    this.draftStorageKey = NEW_CATEGORY_DRAFT_KEY;
    const recovered = this.localStorage.getJSON<AdminCategory>(this.draftStorageKey);
    this.draft.set(recovered ?? this.formFactory.createEmpty());
    this.savedSnapshot = null;
    this.dirty.set(!!recovered);
  }

  loadForEdit(id: string): void {
    this.editorMode.set('edit');
    this.draftStorageKey = `${DRAFT_KEY_PREFIX}${id}`;
    this.gateway.loadCategory(id).pipe(take(1)).subscribe({
      next: category => {
        if (!category) {
          this.draft.set(null);
          return;
        }
        const recovered = this.localStorage.getJSON<AdminCategory>(this.draftStorageKey!);
        this.draft.set(recovered ?? { ...category });
        this.savedSnapshot = JSON.stringify(category);
        this.dirty.set(!!recovered && JSON.stringify(recovered) !== this.savedSnapshot);
      }
    });
  }

  updateDraft(patch: Partial<AdminCategory>): void {
    this.draft.update(current => {
      if (!current) return current;
      const updated = { ...current, ...patch, updatedAt: new Date().toISOString() };
      if (this.draftStorageKey) {
        this.localStorage.setJSON(this.draftStorageKey, updated);
      }
      this.dirty.set(this.savedSnapshot !== JSON.stringify(updated));
      return updated;
    });
    if ('slug' in patch) {
      this.validateSlug();
    }
  }

  validateSlug(): void {
    const draft = this.draft();
    if (!draft || !draft.slug) {
      this.slugTaken.set(false);
      this.slugCheckError.set(false);
      return;
    }
    this.gateway.isSlugTaken(draft.slug, this.editorMode() === 'edit' ? draft.id : null).pipe(
      take(1),
      catchError(() => {
        this.slugCheckError.set(true);
        return of(true);
      })
    ).subscribe(taken => {
      this.slugTaken.set(taken);
      if (!taken) {
        this.slugCheckError.set(false);
      }
    });
  }

  saveDraft(publish: boolean, onSuccess?: () => void): void {
    const draft = this.draft();
    if (!draft || this.slugTaken() || this.slugCheckError()) return;

    this.mutationError.set(null);
    const toSave: AdminCategory = { ...draft, status: publish ? 'published' : 'draft', updatedAt: new Date().toISOString() };
    const request = this.editorMode() === 'create' ? this.gateway.createCategory(toSave) : this.gateway.updateCategory(toSave);

    request.pipe(take(1)).subscribe({
      next: saved => {
        if (this.draftStorageKey) {
          this.localStorage.removeItem(this.draftStorageKey);
        }
        this.draftStorageKey = `${DRAFT_KEY_PREFIX}${saved.id}`;
        this.savedSnapshot = JSON.stringify(saved);
        this.dirty.set(false);
        this.loadList();
        onSuccess?.();
      },
      error: () => this.mutationError.set('common.errorDescription')
    });
  }

  discardDraftRecovery(): void {
    if (!this.draft() || !this.draftStorageKey) return;
    this.localStorage.removeItem(this.draftStorageKey);
  }

  canDelete(id: string): boolean {
    const category = this.categories().find(item => item.id === id);
    return !!category && this.childrenOf(id).length === 0 && category.itemsCount === 0;
  }

  deleteOne(id: string): void {
    this.mutationError.set(null);
    this.gateway.deleteCategory(id).pipe(take(1)).subscribe({
      next: () => { this.loadList(); this.loadDashboardStats(); },
      error: () => this.mutationError.set('common.errorDescription')
    });
  }

  restoreOne(id: string): void {
    this.gateway.restoreCategory(id).pipe(take(1)).subscribe({ next: () => { this.loadList(); this.loadDashboardStats(); } });
  }

  setVisible(id: string, visible: boolean): void {
    const category = this.categories().find(item => item.id === id);
    if (!category) return;
    this.gateway.updateCategory({ ...category, visible, updatedAt: new Date().toISOString() }).pipe(take(1))
      .subscribe({ next: () => { this.loadList(); this.loadDashboardStats(); } });
  }

  reorder(id: string, targetId: string): void {
    const dragged = this.categories().find(item => item.id === id);
    const target = this.categories().find(item => item.id === targetId);
    if (!dragged || !target || dragged.parentId !== target.parentId || dragged.id === target.id) return;

    const siblings = this.categories()
      .filter(item => item.parentId === dragged.parentId)
      .sort((left, right) => left.order - right.order);
    const withoutDragged = siblings.filter(item => item.id !== id);
    const targetIndex = withoutDragged.findIndex(item => item.id === targetId);
    withoutDragged.splice(targetIndex, 0, dragged);

    const now = new Date().toISOString();
    const updates = withoutDragged
      .map((item, index) => ({ item, order: index }))
      .filter(({ item, order }) => item.order !== order)
      .map(({ item, order }) => ({ ...item, order, updatedAt: now }));

    if (updates.length === 0) return;
    updates.forEach(category => this.gateway.updateCategory(category).pipe(take(1)).subscribe());
    this.loadList();
  }

  applyBulkVisibility(visible: boolean): void {
    const selected = new Set(this.selectedIds());
    const updates = this.categories().filter(category => selected.has(category.id)).map(category => ({ ...category, visible, updatedAt: new Date().toISOString() }));
    updates.forEach(category => this.gateway.updateCategory(category).pipe(take(1)).subscribe());
    this.clearSelection();
    this.loadList();
    this.loadDashboardStats();
  }

  applyBulkDelete(): void {
    const ids = [...this.selectedIds()].filter(id => this.canDelete(id));
    ids.forEach(id => this.gateway.deleteCategory(id).pipe(take(1)).subscribe());
    this.clearSelection();
    this.loadList();
    this.loadDashboardStats();
  }

  applyBulkAssignParent(parentId: string | null): void {
    const selected = new Set(this.selectedIds());
    // A category can never become its own ancestor - drop any selected id that is the target parent itself.
    const updates = this.categories()
      .filter(category => selected.has(category.id) && category.id !== parentId)
      .map(category => ({ ...category, parentId, updatedAt: new Date().toISOString() }));
    updates.forEach(category => this.gateway.updateCategory(category).pipe(take(1)).subscribe());
    this.clearSelection();
    this.loadList();
    this.loadDashboardStats();
  }

  applyBulkAssignImage(imageUrl: string): void {
    const selected = new Set(this.selectedIds());
    const updates = this.categories().filter(category => selected.has(category.id)).map(category => ({ ...category, imageUrl, updatedAt: new Date().toISOString() }));
    updates.forEach(category => this.gateway.updateCategory(category).pipe(take(1)).subscribe());
    this.clearSelection();
    this.loadList();
    this.loadDashboardStats();
  }

  /** Duplicates via the existing createCategory call (no dedicated duplicate endpoint exists) - a real create, not a fabricated action. */
  applyBulkDuplicate(): void {
    const selected = new Set(this.selectedIds());
    const sources = this.categories().filter(category => selected.has(category.id));
    sources.forEach(source => {
      const now = new Date().toISOString();
      const duplicated: AdminCategory = {
        ...source,
        id: `category-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
        slug: `${source.slug}-copy-${Date.now()}`,
        title: `${source.title} Copy`,
        status: 'draft',
        createdAt: now,
        updatedAt: now,
      };
      this.gateway.createCategory(duplicated).pipe(take(1)).subscribe();
    });
    this.clearSelection();
    this.loadList();
    this.loadDashboardStats();
  }

  /** Client-side CSV of the currently selected rows - no backend export endpoint exists. */
  exportSelectedAsCsv(): void {
    const selected = new Set(this.selectedIds());
    const rows = this.categories().filter(category => selected.has(category.id));
    const header = ['id', 'title', 'slug', 'parentId', 'visible', 'status', 'itemsCount'];
    const lines = rows.map(category => [category.id, category.title, category.slug, category.parentId ?? '', category.visible, category.status, category.itemsCount]
      .map(value => `"${String(value).replace(/"/g, '""')}"`).join(','));
    const csv = [header.join(','), ...lines].join('\n');
    const blob = new Blob([csv], { type: 'text/csv' });
    const url = URL.createObjectURL(blob);
    const link = document.createElement('a');
    link.href = url;
    link.download = 'categories-export.csv';
    link.click();
    URL.revokeObjectURL(url);
  }

  health(category: AdminCategory): AdminCategoryHealth {
    const hasImage = category.imageUrl.trim().length > 0;
    const hasSeo = !!(category.seo.metaTitle.trim() && category.seo.metaDescription.trim());
    const hasDescription = category.description.trim().length > 0;
    const hasValidParent = category.parentId === null || this.categories().some(other => other.id === category.parentId);
    const isVisible = category.visible;
    const hasProducts = category.itemsCount > 0;
    const checks = [hasImage, hasSeo, hasDescription, hasValidParent, isVisible, hasProducts];
    const completionPercent = Math.round((checks.filter(Boolean).length / checks.length) * 100);
    return { hasImage, hasSeo, hasDescription, hasValidParent, isVisible, hasProducts, completionPercent };
  }

  readonly dashboardStats = signal<AdminCategoriesDashboardStats | null>(null);

  /** Dashboard stats reflect the whole catalog, not the current filtered view. */
  loadDashboardStats(): void {
    this.gateway.loadCategories({ search: '', visibility: 'all', includeDeleted: false }).pipe(take(1))
      .subscribe({ next: categories => this.dashboardStats.set(this.computeDashboardStats(categories)) });
  }

  private computeDashboardStats(categories: AdminCategory[]): AdminCategoriesDashboardStats {
    const withHealth = categories.map(category => ({ category, health: this.health(category) }));
    const missingImages = withHealth.filter(entry => !entry.health.hasImage);
    const missingSeo = withHealth.filter(entry => !entry.health.hasSeo);
    const empty = categories.filter(category => category.itemsCount === 0);
    const lastModifiedAt = categories.reduce<string | null>((latest, category) => !latest || category.updatedAt > latest ? category.updatedAt : latest, null);
    const avgCompletion = withHealth.length > 0
      ? Math.round(withHealth.reduce((sum, entry) => sum + entry.health.completionPercent, 0) / withHealth.length)
      : 0;

    let recommendation: AdminCategoriesDashboardStats['recommendation'];
    if (missingImages[0]) {
      recommendation = { labelKey: 'adminCategories.recommendAddImage', categoryId: missingImages[0].category.id };
    } else if (missingSeo[0]) {
      recommendation = { labelKey: 'adminCategories.recommendAddSeo', categoryId: missingSeo[0].category.id };
    } else if (empty[0]) {
      recommendation = { labelKey: 'adminCategories.recommendFillEmpty', categoryId: empty[0].id };
    } else {
      recommendation = { labelKey: 'adminCategories.recommendNone' };
    }

    return {
      total: categories.length,
      visible: categories.filter(category => category.visible).length,
      hidden: categories.filter(category => !category.visible).length,
      empty: empty.length,
      rootCount: categories.filter(category => !category.parentId).length,
      subCount: categories.filter(category => !!category.parentId).length,
      missingImages: missingImages.length,
      missingSeo: missingSeo.length,
      lastModifiedAt,
      completionPercent: avgCompletion,
      recommendation,
    };
  }
}
