import {DOCUMENT} from '@angular/common';
import {HttpClient} from '@angular/common/http';
import {
  computed,
  DestroyRef,
  effect,
  inject,
  Injectable,
  Renderer2,
  RendererFactory2,
  SecurityContext
} from '@angular/core';
import {DomSanitizer} from '@angular/platform-browser';
import {ToastrService} from 'ngx-toastr';
import {EMPTY, map, of, ReplaySubject, take, tap} from 'rxjs';
import {environment} from 'src/environments/environment';
import {ConfirmService} from '../shared/confirm.service';
import {NotificationProgressEvent} from '../_models/events/notification-progress-event';
import {SiteTheme, ThemeProvider} from '../_models/preferences/site-theme';
import {TextResonse} from '../_types/text-response';
import {EVENTS, MessageHubService} from './message-hub.service';
import {takeUntilDestroyed, toSignal} from "@angular/core/rxjs-interop";
import {translate} from "@jsverse/transloco";
import {DownloadableSiteTheme} from "../_models/theme/downloadable-site-theme";
import {NgxFileDropEntry} from "ngx-file-drop";
import {SiteThemeUpdatedEvent} from "../_models/events/site-theme-updated-event";
import {ColorscapeService} from "./colorscape.service";
import {ColorScape} from "../_models/theme/colorscape";
import {AccountService} from "./account.service";

@Injectable({
  providedIn: 'root'
})
export class ThemeService {
  private document = inject<Document>(DOCUMENT);
  private httpClient = inject(HttpClient);
  private domSanitizer = inject(DomSanitizer);
  private confirmService = inject(ConfirmService);
  private toastr = inject(ToastrService);
  private accountService = inject(AccountService);


  private readonly destroyRef = inject(DestroyRef);
  private readonly colorTransitionService = inject(ColorscapeService);

  public defaultTheme: string = 'dark';
  public defaultBookTheme: string = 'Dark';

  private currentThemeSource = new ReplaySubject<SiteTheme>(1);
  public currentTheme$ = this.currentThemeSource.asObservable();
  public currentTheme = toSignal(this.currentTheme$);
  public chartsColourPalette = computed(() => this.loadChartColours(this.currentTheme()));

  private themesSource = new ReplaySubject<SiteTheme[]>(1);
  public themes$ = this.themesSource.asObservable();

  private darkModeSource = new ReplaySubject<boolean>(1);
  public isDarkMode$ = this.darkModeSource.asObservable();

  /**
   * Maintain a cache of themes. SignalR will inform us if we need to refresh cache
   */
  private themeCache: Array<SiteTheme> = [];

  private renderer: Renderer2;
  private baseUrl = environment.apiUrl;


  constructor() {
    const rendererFactory = inject(RendererFactory2);
    const messageHub = inject(MessageHubService);

    this.renderer = rendererFactory.createRenderer(null, null);

    messageHub.messages$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(message => {

      if (message.event === EVENTS.NotificationProgress) {
        const notificationEvent = (message.payload as NotificationProgressEvent);
        if (notificationEvent.name !== EVENTS.SiteThemeProgress) return;

        if (notificationEvent.eventType === 'ended') {
          if (notificationEvent.name === EVENTS.SiteThemeProgress) this.getThemes().subscribe();
        }
        return;
      }

      if (message.event === EVENTS.SiteThemeUpdated) {
        const evt = (message.payload as SiteThemeUpdatedEvent);
        this.currentTheme$.pipe(take(1)).subscribe(currentTheme => {
          if (currentTheme && currentTheme.name !== EVENTS.SiteThemeProgress) return;

          console.log('Active theme has been updated, refreshing theme');
          this.setTheme(currentTheme.name);
        });
      }
    });

    effect(() => {
      const user = this.accountService.currentUser();
      if (user?.preferences && user?.preferences.theme) {
        this.setTheme(user.preferences.theme.name);
      } else {
        this.setTheme(this.defaultTheme);
      }
    });
  }

  getDownloadableThemes() {
    return this.httpClient.get<Array<DownloadableSiteTheme>>(this.baseUrl + 'theme/browse');
  }

  downloadTheme(theme: DownloadableSiteTheme) {
    return this.httpClient.post<SiteTheme>(this.baseUrl + 'theme/download-theme', theme);
  }

  uploadTheme(themeFile: File, fileEntry: NgxFileDropEntry) {
    const formData = new FormData()
    formData.append('formFile', themeFile, fileEntry.relativePath);

    return this.httpClient.post<SiteTheme>(this.baseUrl + 'theme/upload-theme', formData);
  }

  getColorScheme() {
    return getComputedStyle(this.document.body).getPropertyValue('--color-scheme').trim();
  }

  /**
   * --theme-color from theme. Updates the meta tag
   * @returns
   */
  getThemeColor() {
    return getComputedStyle(this.document.body).getPropertyValue('--theme-color').trim();
  }

  /**
   * --msapplication-TileColor from theme. Updates the meta tag
   * @returns
   */
  getTileColor() {
    return getComputedStyle(this.document.body).getPropertyValue('--title-color').trim();
  }

  getCssVariable(variable: string) {
    return getComputedStyle(this.document.body).getPropertyValue(variable).trim();
  }

  isDarkTheme() {
    return this.getColorScheme().toLowerCase() === 'dark';
  }

  getThemes() {
    if (!this.accountService.isLoggedIn()) return EMPTY;

    return this.httpClient.get<SiteTheme[]>(this.baseUrl + 'theme').pipe(map(themes => {
      this.themeCache = themes;
      this.themesSource.next(themes);
      this.currentTheme$.pipe(take(1)).subscribe(theme => {
        if (themes.filter(t => t.id === theme.id).length === 0) {
          this.setTheme(this.defaultTheme);
          this.toastr.info(translate('toasts.theme-missing'));
        }
      });
      return themes;
    }));
  }

  /**
   * Used in book reader to remove all themes so book reader can provide custom theming options
   */
  clearThemes() {
    this.unsetThemes();
  }

  deleteTheme(themeId: number) {
    return this.httpClient.delete(this.baseUrl + 'theme?themeId=' + themeId).pipe(map(() => {
      this.getThemes().subscribe(() => {});
    }));
  }

  setDefault(themeId: number) {
    return this.httpClient.post(this.baseUrl + 'theme/update-default', {themeId: themeId}).pipe(map(() => {
      // Refresh the cache when a default state is changed
      this.getThemes().subscribe(() => {});
    }));
  }

  /**
   * Sets the book theme on the body tag so css variable overrides can take place
   * @param selector brtheme- prefixed string
   */
  setBookTheme(selector: string) {
    this.unsetBookThemes();
    this.renderer.addClass(this.document.querySelector('body'), selector);
  }

  clearBookTheme() {
    this.unsetBookThemes();
  }


  /**
   * Set's the background color from a single primary color.
   * @param primaryColor
   * @param complementaryColor
   */
  setColorScape(primaryColor: string, complementaryColor: string | null = null) {
    this.colorTransitionService.setColorScape(primaryColor, complementaryColor);
  }

  /**
   * Trigger a request to get the colors for a given entity and apply them
   * @param entity
   * @param id
   */
  refreshColorScape(entity: 'series' | 'volume' | 'chapter' | 'person', id: number) {
    return this.httpClient.get<ColorScape>(`${this.baseUrl}colorscape/${entity}?id=${id}`).pipe(tap((cs) => {
      this.setColorScape(cs.primary || '', cs.secondary);
    }));
  }

  /**
   * Sets the theme as active. Will inject a style tag into document to load a custom theme and apply the selector to the body
   * @param themeName
   */
   setTheme(themeName: string) {
    const theme = this.themeCache.find(t => t.name.toLowerCase() === themeName.toLowerCase());
    if (theme) {
      this.unsetThemes();
      this.renderer.addClass(this.document.querySelector('body'), theme.selector);

      if (theme.provider !== ThemeProvider.System && !this.hasThemeInHead(theme.name)) {
        // We need to load the styles into the browser
        this.fetchThemeContent(theme.id).subscribe(async (content) => {
          if (content === null) {
            await this.confirmService.alert(translate('toasts.alert-bad-theme'));
            this.setTheme('dark');
            return;
          }
          this.injectStyleNode(theme, content);
          this.updateMetaTags();
          this.currentThemeSource.next(theme);
          this.darkModeSource.next(this.isDarkTheme());
        });
      } else {
        this.updateMetaTags();
        this.currentThemeSource.next(theme);
        this.darkModeSource.next(this.isDarkTheme());
      }
    } else {
      // Only time themes isn't already loaded is on first load
      this.getThemes().subscribe(themes => {
        this.setTheme(themeName);
      });
    }
  }

  private updateMetaTags() {
    const themeColor = this.getThemeColor();
    if (themeColor) {
      this.document.querySelector('meta[name="theme-color"]')?.setAttribute('content', themeColor);
      this.document.querySelector('meta[name="apple-mobile-web-app-status-bar-style"]')?.setAttribute('content', this.isDarkTheme() ? 'black' : 'default');
      this.document.querySelector('meta[name="color-scheme"]')?.setAttribute('content', this.isDarkTheme() ? 'dark' : 'light');
    }

    const tileColor = this.getTileColor();
    if (tileColor) {
      this.document.querySelector('meta[name="msapplication-TileColor"]')?.setAttribute('content', tileColor);
    }

    const colorScheme = this.getColorScheme();
    if (colorScheme) {
      this.document.querySelector('body')?.setAttribute('theme', colorScheme);
    }
  }

  private hasThemeInHead(themeName: string) {
    const id = 'theme-' + themeName.toLowerCase();
    return Array.from(this.document.head.children).filter(el => el.tagName === 'STYLE' && el.id.toLowerCase() === id).length > 0;
  }

  private fetchThemeContent(themeId: number) {
    return this.httpClient.get<string>(this.baseUrl + 'theme/download-content?themeId=' + themeId, TextResonse).pipe(map(encodedCss => {
      return this.domSanitizer.sanitize(SecurityContext.STYLE, encodedCss);
    }));
  }

  private unsetThemes() {
    this.themeCache.forEach(theme => {
      this.document.body.classList.remove(theme.selector);


      // TODO: BUG: We aren't removing the old style tags, thus some style conflict can occur (switch theme, delete button might be green).
      // We need to remove the old style tags, however this brought instability in v0.8.9. I removed this for hotfix, let's revisit it later.
      // Remove the injected style (unless it's Dark)
      // if (theme.provider === ThemeProvider.System) return;
      // const styleElem = this.document.querySelector('style#theme-' + theme.name);
      // if (styleElem) {
      //   this.renderer.removeChild(this.document.head, styleElem);
      // }
    });
  }

  private injectStyleNode(theme: SiteTheme, content: string) {
    const styleElem = this.document.createElement('style');
    styleElem.id = 'theme-' + theme.name;
    styleElem.appendChild(this.document.createTextNode(content));
    this.renderer.appendChild(this.document.head, styleElem);
  }

  private unsetBookThemes() {
    Array.from(this.document.body.classList).filter(cls => cls.startsWith('brtheme-')).forEach(c => this.document.body.classList.remove(c));
  }

  private loadChartColours(_: SiteTheme | undefined) {
     return [
       '--charts-palette1',
       '--charts-palette2',
       '--charts-palette3',
       '--charts-palette4',
       '--charts-palette5',
       '--charts-palette6',
       '--charts-palette7',
     ].map(ccsVarName => this.getCssVariable(ccsVarName))
  }
}
