import {
  Injectable,
  computed,
  inject,
  signal,
  type Signal
} from '@angular/core';
import { v4 as uuidv4 } from 'uuid';
import { environment } from '../../../../../environments/environment';
import {
  buildDefaultEndpointTemplates,
  buildDefaultServerDefinitions,
  ensureAnyActiveEndpoint,
  ensureCompatibleActiveEndpoint,
  findDefaultEndpointKeyByUrl,
  hasEndpointForDefault,
  matchDefaultEndpointTemplate,
  sanitiseServerBaseUrl
} from '../../domain/logic/server-endpoint-defaults.logic';
import { ServerEndpointStorageService } from '../../infrastructure/services/server-endpoint-storage.service';
import type {
  ConfiguredDefaultServerDefinition,
  DefaultEndpointTemplate,
  ServerEndpoint,
  ServerEndpointVersions
} from '../../domain/models/server-directory.model';

function resolveDefaultHttpProtocol(): 'http' | 'https' {
  return typeof window !== 'undefined' && window.location?.protocol === 'https:'
    ? 'https'
    : 'http';
}

@Injectable({ providedIn: 'root' })
export class ServerEndpointStateService {
  readonly servers: Signal<ServerEndpoint[]>;
  readonly activeServers: Signal<ServerEndpoint[]>;
  readonly hasMissingDefaultServers: Signal<boolean>;
  readonly activeServer: Signal<ServerEndpoint | null>;

  private readonly storage = inject(ServerEndpointStorageService);
  private readonly _servers = signal<ServerEndpoint[]>([]);
  private readonly defaultEndpoints: DefaultEndpointTemplate[];
  private readonly primaryDefaultServerUrl: string;

  constructor() {
    const defaultServerDefinitions = buildDefaultServerDefinitions(
      Array.isArray(environment.defaultServers)
        ? environment.defaultServers as ConfiguredDefaultServerDefinition[]
        : [],
      environment.defaultServerUrl,
      resolveDefaultHttpProtocol()
    );

    this.defaultEndpoints = buildDefaultEndpointTemplates(defaultServerDefinitions);
    this.primaryDefaultServerUrl = this.defaultEndpoints[0]?.url ?? 'http://localhost:3001';

    this.servers = computed(() => this._servers());
    this.activeServers = computed(() =>
      this._servers().filter((endpoint) => endpoint.isActive && endpoint.status !== 'incompatible')
    );

    this.hasMissingDefaultServers = computed(() =>
      this.defaultEndpoints.some((endpoint) => !hasEndpointForDefault(this._servers(), endpoint))
    );

    this.activeServer = computed(() => this.activeServers()[0] ?? null);

    this.loadEndpoints();
  }

  getPrimaryDefaultServerUrl(): string {
    return this.primaryDefaultServerUrl;
  }

  sanitiseUrl(rawUrl: string): string {
    return sanitiseServerBaseUrl(rawUrl);
  }

  addServer(server: { name: string; url: string }): ServerEndpoint {
    const newEndpoint: ServerEndpoint = {
      id: uuidv4(),
      name: server.name,
      url: this.sanitiseUrl(server.url),
      isActive: true,
      isDefault: false,
      status: 'unknown'
    };

    this._servers.update((endpoints) => [...endpoints, newEndpoint]);
    this.saveEndpoints();
    return newEndpoint;
  }

  ensureServerEndpoint(
    server: { name: string; url: string },
    options?: { setActive?: boolean }
  ): ServerEndpoint {
    const existing = this.findServerByUrl(server.url);

    if (existing) {
      if (options?.setActive) {
        this.setActiveServer(existing.id);
      }

      return existing;
    }

    const created = this.addServer(server);

    if (options?.setActive) {
      this.setActiveServer(created.id);
    }

    return created;
  }

  findServerByUrl(url: string): ServerEndpoint | undefined {
    const sanitisedUrl = this.sanitiseUrl(url);
    const exactEndpoint = this._servers().find((endpoint) => this.sanitiseUrl(endpoint.url) === sanitisedUrl);

    return exactEndpoint ?? this.findHttpEndpointForHttpsUrl(sanitisedUrl);
  }

  resolveCanonicalEndpoint(endpoint: ServerEndpoint | null | undefined): ServerEndpoint | null {
    if (!endpoint?.instanceId) {
      return endpoint ?? null;
    }

    const equivalentEndpoints = this._servers().filter((candidate) => candidate.instanceId === endpoint.instanceId);

    if (equivalentEndpoints.length <= 1) {
      return endpoint;
    }

    return [...equivalentEndpoints].sort((left, right) => this.compareEndpointPreference(left, right))[0] ?? endpoint;
  }

  removeServer(endpointId: string): void {
    const endpoints = this._servers();
    const target = endpoints.find((endpoint) => endpoint.id === endpointId);

    if (!target || endpoints.length <= 1) {
      return;
    }

    if (target.isDefault) {
      this.markDefaultEndpointRemoved(target);
      this.clearDefaultEndpointDisabled(target);
    }

    const updatedEndpoints = ensureAnyActiveEndpoint(
      endpoints.filter((endpoint) => endpoint.id !== endpointId)
    );

    this._servers.set(updatedEndpoints);
    this.saveEndpoints();
  }

  restoreDefaultServers(): ServerEndpoint[] {
    const restoredEndpoints = this.defaultEndpoints
      .filter((defaultEndpoint) => !hasEndpointForDefault(this._servers(), defaultEndpoint))
      .map((defaultEndpoint) => ({
        ...defaultEndpoint,
        id: uuidv4(),
        isActive: true
      }));

    if (restoredEndpoints.length === 0) {
      this.storage.clearRemovedDefaultEndpointKeys();
      return [];
    }

    this._servers.update((endpoints) => ensureAnyActiveEndpoint([...endpoints, ...restoredEndpoints]));
    this.storage.clearRemovedDefaultEndpointKeys();
    this.clearDisabledDefaultEndpointKeys(restoredEndpoints);
    this.saveEndpoints();
    return restoredEndpoints;
  }

  setActiveServer(endpointId: string): void {
    this._servers.update((endpoints) => {
      const target = endpoints.find((endpoint) => endpoint.id === endpointId);

      if (!target || target.status === 'incompatible') {
        return endpoints;
      }

      return endpoints.map((endpoint) =>
        endpoint.id === endpointId
          ? { ...endpoint, isActive: true }
          : endpoint
      );
    });

    const target = this._servers().find((endpoint) => endpoint.id === endpointId);

    if (target?.isDefault) {
      this.clearDefaultEndpointDisabled(target);
    }

    this.saveEndpoints();
  }

  deactivateServer(endpointId: string): void {
    if (this.activeServers().length <= 1) {
      return;
    }

    this._servers.update((endpoints) =>
      endpoints.map((endpoint) =>
        endpoint.id === endpointId
          ? { ...endpoint, isActive: false }
          : endpoint
      )
    );

    const target = this._servers().find((endpoint) => endpoint.id === endpointId);

    if (target?.isDefault) {
      this.markDefaultEndpointDisabled(target);
    }

    this.saveEndpoints();
  }

  updateServerStatus(
    endpointId: string,
    status: ServerEndpoint['status'],
    latency?: number,
    versions?: ServerEndpointVersions
  ): void {
    this._servers.update((endpoints) => ensureCompatibleActiveEndpoint(endpoints.map((endpoint) => {
      if (endpoint.id !== endpointId) {
        return endpoint;
      }

      return {
        ...endpoint,
        instanceId: versions?.serverInstanceId ?? endpoint.instanceId,
        status,
        latency,
        isActive: status === 'incompatible' && !endpoint.isDefault ? false : endpoint.isActive,
        serverVersion: versions?.serverVersion ?? endpoint.serverVersion,
        clientVersion: versions?.clientVersion ?? endpoint.clientVersion
      };
    })));

    this.saveEndpoints();
  }

  private loadEndpoints(): void {
    const storedEndpoints = this.storage.loadEndpoints();

    if (!storedEndpoints) {
      this.initialiseDefaultEndpoints();
      return;
    }

    this._servers.set(this.reconcileStoredEndpoints(storedEndpoints));
    this.saveEndpoints();
  }

  private initialiseDefaultEndpoints(): void {
    this._servers.set(this.defaultEndpoints.map((endpoint) => ({
      ...endpoint,
      id: uuidv4()
    })));

    this.saveEndpoints();
  }

  private reconcileStoredEndpoints(storedEndpoints: ServerEndpoint[]): ServerEndpoint[] {
    const reconciled: ServerEndpoint[] = [];
    const claimedDefaultKeys = new Set<string>();
    const disabledDefaultKeys = this.storage.loadDisabledDefaultEndpointKeys();
    const removedDefaultKeys = this.storage.loadRemovedDefaultEndpointKeys();

    for (const endpoint of storedEndpoints) {
      if (!endpoint || typeof endpoint.id !== 'string' || typeof endpoint.url !== 'string') {
        continue;
      }

      const sanitisedUrl = this.sanitiseUrl(endpoint.url);
      const matchedDefault = matchDefaultEndpointTemplate(
        this.defaultEndpoints,
        endpoint,
        sanitisedUrl,
        claimedDefaultKeys
      );

      if (matchedDefault) {
        claimedDefaultKeys.add(matchedDefault.defaultKey);
        reconciled.push({
          ...endpoint,
          name: matchedDefault.name,
          url: matchedDefault.url,
          isActive: this.isDefaultEndpointActive(matchedDefault.defaultKey, disabledDefaultKeys),
          isDefault: true,
          defaultKey: matchedDefault.defaultKey,
          status: endpoint.status ?? 'unknown'
        });

        continue;
      }

      reconciled.push({
        ...endpoint,
        url: sanitisedUrl,
        status: endpoint.status ?? 'unknown'
      });
    }

    for (const defaultEndpoint of this.defaultEndpoints) {
      if (
        !claimedDefaultKeys.has(defaultEndpoint.defaultKey)
        && !removedDefaultKeys.has(defaultEndpoint.defaultKey)
        && !hasEndpointForDefault(reconciled, defaultEndpoint)
      ) {
        reconciled.push({
          ...defaultEndpoint,
          id: uuidv4(),
          isActive: this.isDefaultEndpointActive(defaultEndpoint.defaultKey, disabledDefaultKeys)
        });
      }
    }

    return ensureAnyActiveEndpoint(reconciled);
  }

  private markDefaultEndpointRemoved(endpoint: ServerEndpoint): void {
    const defaultKey = endpoint.defaultKey ?? findDefaultEndpointKeyByUrl(this.defaultEndpoints, endpoint.url);

    if (!defaultKey) {
      return;
    }

    const removedDefaultKeys = this.storage.loadRemovedDefaultEndpointKeys();

    removedDefaultKeys.add(defaultKey);
    this.storage.saveRemovedDefaultEndpointKeys(removedDefaultKeys);
  }

  private markDefaultEndpointDisabled(endpoint: ServerEndpoint): void {
    const defaultKey = endpoint.defaultKey ?? findDefaultEndpointKeyByUrl(this.defaultEndpoints, endpoint.url);

    if (!defaultKey) {
      return;
    }

    const disabledDefaultKeys = this.storage.loadDisabledDefaultEndpointKeys();

    disabledDefaultKeys.add(defaultKey);
    this.storage.saveDisabledDefaultEndpointKeys(disabledDefaultKeys);
  }

  private clearDefaultEndpointDisabled(endpoint: ServerEndpoint): void {
    const defaultKey = endpoint.defaultKey ?? findDefaultEndpointKeyByUrl(this.defaultEndpoints, endpoint.url);

    if (!defaultKey) {
      return;
    }

    const disabledDefaultKeys = this.storage.loadDisabledDefaultEndpointKeys();

    if (!disabledDefaultKeys.delete(defaultKey)) {
      return;
    }

    this.storage.saveDisabledDefaultEndpointKeys(disabledDefaultKeys);
  }

  private clearDisabledDefaultEndpointKeys(endpoints: ServerEndpoint[]): void {
    const disabledDefaultKeys = this.storage.loadDisabledDefaultEndpointKeys();

    let didChange = false;

    for (const endpoint of endpoints) {
      const defaultKey = endpoint.defaultKey ?? findDefaultEndpointKeyByUrl(this.defaultEndpoints, endpoint.url);

      if (!defaultKey) {
        continue;
      }

      didChange = disabledDefaultKeys.delete(defaultKey) || didChange;
    }

    if (!didChange) {
      return;
    }

    this.storage.saveDisabledDefaultEndpointKeys(disabledDefaultKeys);
  }

  private isDefaultEndpointActive(
    defaultKey: string,
    disabledDefaultKeys: Set<string>
  ): boolean {
    return !disabledDefaultKeys.has(defaultKey);
  }

  private saveEndpoints(): void {
    this.storage.saveEndpoints(this._servers());
  }

  private compareEndpointPreference(left: ServerEndpoint, right: ServerEndpoint): number {
    const scoreDifference = this.endpointPreferenceScore(right) - this.endpointPreferenceScore(left);

    if (scoreDifference !== 0) {
      return scoreDifference;
    }

    return left.url.localeCompare(right.url);
  }

  private endpointPreferenceScore(endpoint: ServerEndpoint): number {
    let score = 0;

    if (endpoint.isDefault) {
      score += 4;
    }

    if (this.usesSecureProtocol(endpoint.url)) {
      score += 2;
    }

    if (!this.isIpHost(endpoint.url)) {
      score += 1;
    }

    return score;
  }

  private usesSecureProtocol(url: string): boolean {
    try {
      return new URL(url).protocol === 'https:';
    } catch {
      return false;
    }
  }

  private isIpHost(url: string): boolean {
    try {
      const hostname = new URL(url).hostname;

      return /^\d{1,3}(?:\.\d{1,3}){3}$/.test(hostname) || hostname.includes(':');
    } catch {
      return false;
    }
  }

  private findHttpEndpointForHttpsUrl(url: string): ServerEndpoint | undefined {
    const requestedUrl = this.parseUrl(url);

    if (requestedUrl?.protocol !== 'https:') {
      return undefined;
    }

    return this._servers().find((endpoint) => {
      const endpointUrl = this.parseUrl(endpoint.url);

      return endpointUrl?.protocol === 'http:'
        && endpointUrl.hostname === requestedUrl.hostname
        && endpointUrl.port === requestedUrl.port;
    });
  }

  private parseUrl(url: string): URL | null {
    try {
      return new URL(url);
    } catch {
      return null;
    }
  }
}
