import * as plugins from '../plugins.ts';
import { logger } from '../logging.ts';
import type * as interfaces from '../../ts_interfaces/index.ts';
import { BaseProvider, GiteaProvider, GitLabProvider } from '../providers/index.ts';
import type { StorageManager } from '../storage/index.ts';

const LEGACY_CONNECTIONS_FILE = './.nogit/connections.json';
const CONNECTIONS_PREFIX = '/connections/';
const KEYCHAIN_PREFIX = 'keychain:';

/**
 * Manages provider connections — persists each connection as an
 * individual JSON file via StorageManager. Tokens are stored in
 * the OS keychain (or encrypted file fallback) via SmartSecret.
 */
export class ConnectionManager {
  private connections: interfaces.data.IProviderConnection[] = [];
  private storageManager: StorageManager;
  private smartSecret: plugins.smartsecret.SmartSecret;
  /** Resolves when background connection health checks complete */
  public healthCheckDone: Promise<void> = Promise.resolve();

  constructor(storageManager: StorageManager, smartSecret: plugins.smartsecret.SmartSecret) {
    this.storageManager = storageManager;
    this.smartSecret = smartSecret;
  }

  async init(): Promise<void> {
    await this.migrateLegacyFile();
    await this.loadConnections();
    // Auto-test all connections in the background
    this.healthCheckDone = this.testAllConnections();
  }

  /**
   * Tests all loaded connections in the background and updates their status.
   * Fire-and-forget — does not block startup.
   */
  private async testAllConnections(): Promise<void> {
    for (const conn of this.connections) {
      if (conn.status === 'paused') continue;
      try {
        const provider = this.getProvider(conn.id);
        const result = await provider.testConnection();
        conn.status = result.ok ? 'connected' : 'error';
        await this.persistConnection(conn);
      } catch {
        conn.status = 'error';
      }
    }
  }

  /**
   * One-time migration from the legacy .nogit/connections.json file.
   */
  private async migrateLegacyFile(): Promise<void> {
    try {
      const text = await Deno.readTextFile(LEGACY_CONNECTIONS_FILE);
      const legacy: interfaces.data.IProviderConnection[] = JSON.parse(text);
      if (legacy.length > 0) {
        logger.info(`Migrating ${legacy.length} connection(s) from legacy file...`);
        for (const conn of legacy) {
          await this.storageManager.setJSON(`${CONNECTIONS_PREFIX}${conn.id}.json`, conn);
        }
        // Rename legacy file so migration doesn't repeat
        await Deno.rename(LEGACY_CONNECTIONS_FILE, LEGACY_CONNECTIONS_FILE + '.migrated');
        logger.success('Legacy connections migrated successfully');
      }
    } catch {
      // No legacy file or already migrated — nothing to do
    }
  }

  private async loadConnections(): Promise<void> {
    const keys = await this.storageManager.list(CONNECTIONS_PREFIX);
    this.connections = [];
    for (const key of keys) {
      const conn = await this.storageManager.getJSON<interfaces.data.IProviderConnection>(key);
      if (conn) {
        // Migrate legacy baseGroup/baseGroupId property names
        if ((conn as any).baseGroup !== undefined && conn.groupFilter === undefined) {
          conn.groupFilter = (conn as any).baseGroup;
          delete (conn as any).baseGroup;
        }
        if ((conn as any).baseGroupId !== undefined && conn.groupFilterId === undefined) {
          conn.groupFilterId = (conn as any).baseGroupId;
          delete (conn as any).baseGroupId;
        }
        if (conn.token.startsWith(KEYCHAIN_PREFIX)) {
          // Token is in keychain — retrieve it
          const realToken = await this.smartSecret.getSecret(conn.id);
          if (realToken) {
            conn.token = realToken;
          } else {
            logger.warn(`Could not retrieve token for connection ${conn.id} from keychain`);
          }
        } else if (conn.token && conn.token !== '***') {
          // Plaintext token found — auto-migrate to keychain
          await this.migrateTokenToKeychain(conn);
        }
        this.connections.push(conn);
      }
    }
    if (this.connections.length > 0) {
      logger.info(`Loaded ${this.connections.length} connection(s)`);
    } else {
      logger.debug('No existing connections found, starting fresh');
    }
  }

  /**
   * Migrates a plaintext token to keychain storage.
   */
  private async migrateTokenToKeychain(
    conn: interfaces.data.IProviderConnection,
  ): Promise<void> {
    try {
      await this.smartSecret.setSecret(conn.id, conn.token);
      // Save sentinel to JSON file
      const jsonConn = { ...conn, token: `${KEYCHAIN_PREFIX}${conn.id}` };
      await this.storageManager.setJSON(`${CONNECTIONS_PREFIX}${conn.id}.json`, jsonConn);
      logger.info(`Migrated token for connection "${conn.name}" to keychain`);
    } catch (err) {
      logger.warn(`Failed to migrate token for ${conn.id} to keychain: ${err}`);
    }
  }

  private async persistConnection(conn: interfaces.data.IProviderConnection): Promise<void> {
    // Store real token in keychain
    await this.smartSecret.setSecret(conn.id, conn.token);
    // Save JSON with sentinel value
    const jsonConn = { ...conn, token: `${KEYCHAIN_PREFIX}${conn.id}` };
    await this.storageManager.setJSON(`${CONNECTIONS_PREFIX}${conn.id}.json`, jsonConn);
  }

  private async removeConnection(id: string): Promise<void> {
    await this.smartSecret.deleteSecret(id);
    await this.storageManager.delete(`${CONNECTIONS_PREFIX}${id}.json`);
  }

  getConnections(): interfaces.data.IProviderConnection[] {
    return this.connections.map((c) => ({ ...c, token: '***' }));
  }

  getConnection(id: string): interfaces.data.IProviderConnection | undefined {
    return this.connections.find((c) => c.id === id);
  }

  async createConnection(
    name: string,
    providerType: interfaces.data.TProviderType,
    baseUrl: string,
    token: string,
    groupFilter?: string,
  ): Promise<interfaces.data.IProviderConnection> {
    const connection: interfaces.data.IProviderConnection = {
      id: crypto.randomUUID(),
      name,
      providerType,
      baseUrl: baseUrl.replace(/\/+$/, ''),
      token,
      createdAt: Date.now(),
      status: 'disconnected',
      groupFilter: groupFilter || undefined,
    };
    this.connections.push(connection);
    await this.persistConnection(connection);
    logger.success(`Connection created: ${name} (${providerType})`);
    return { ...connection, token: '***' };
  }

  async updateConnection(
    id: string,
    updates: { name?: string; baseUrl?: string; token?: string; groupFilter?: string },
  ): Promise<interfaces.data.IProviderConnection> {
    const conn = this.connections.find((c) => c.id === id);
    if (!conn) throw new Error(`Connection not found: ${id}`);
    if (updates.name) conn.name = updates.name;
    if (updates.baseUrl) conn.baseUrl = updates.baseUrl.replace(/\/+$/, '');
    if (updates.token) conn.token = updates.token;
    if (updates.groupFilter !== undefined) {
      conn.groupFilter = updates.groupFilter || undefined;
      conn.groupFilterId = undefined; // Will be re-resolved on next test
    }
    await this.persistConnection(conn);
    return { ...conn, token: '***' };
  }

  async deleteConnection(id: string): Promise<void> {
    const idx = this.connections.findIndex((c) => c.id === id);
    if (idx === -1) throw new Error(`Connection not found: ${id}`);
    this.connections.splice(idx, 1);
    await this.removeConnection(id);
    logger.info(`Connection deleted: ${id}`);
  }

  async pauseConnection(id: string, paused: boolean): Promise<interfaces.data.IProviderConnection> {
    const conn = this.connections.find((c) => c.id === id);
    if (!conn) throw new Error(`Connection not found: ${id}`);
    conn.status = paused ? 'paused' : 'disconnected';
    await this.persistConnection(conn);
    logger.info(`Connection ${paused ? 'paused' : 'resumed'}: ${conn.name}`);
    return { ...conn, token: '***' };
  }

  async testConnection(id: string): Promise<{ ok: boolean; error?: string }> {
    const conn = this.connections.find((c) => c.id === id)!;
    if (conn.status === 'paused') {
      return { ok: false, error: 'Connection is paused' };
    }
    const provider = this.getProvider(id);
    const result = await provider.testConnection();
    conn.status = result.ok ? 'connected' : 'error';
    // Resolve group filter ID if connection has a groupFilter
    if (result.ok && conn.groupFilter) {
      await this.resolveGroupFilterId(conn);
    }
    await this.persistConnection(conn);
    return result;
  }

  /**
   * Resolves a human-readable groupFilter to the provider-specific group ID.
   */
  private async resolveGroupFilterId(conn: interfaces.data.IProviderConnection): Promise<void> {
    if (!conn.groupFilter) {
      conn.groupFilterId = undefined;
      return;
    }
    try {
      if (conn.providerType === 'gitlab') {
        const gitlabClient = new plugins.gitlabClient.GitLabClient(conn.baseUrl, conn.token);
        const group = await gitlabClient.getGroup(conn.groupFilter);
        conn.groupFilterId = String(group.id);
        logger.info(`Resolved group filter "${conn.groupFilter}" to ID ${conn.groupFilterId}`);
      } else {
        // For Gitea, the org name IS the ID
        conn.groupFilterId = conn.groupFilter;
        logger.info(`Group filter for Gitea connection set to org "${conn.groupFilterId}"`);
      }
    } catch (err) {
      logger.warn(`Failed to resolve group filter "${conn.groupFilter}": ${err}`);
      conn.groupFilterId = undefined;
    }
  }

  /**
   * Factory: returns the correct provider instance for a connection ID
   */
  getProvider(connectionId: string): BaseProvider {
    const conn = this.connections.find((c) => c.id === connectionId);
    if (!conn) throw new Error(`Connection not found: ${connectionId}`);

    switch (conn.providerType) {
      case 'gitea':
        return new GiteaProvider(conn.id, conn.baseUrl, conn.token, conn.groupFilterId);
      case 'gitlab':
        return new GitLabProvider(conn.id, conn.baseUrl, conn.token, conn.groupFilterId);
      default:
        throw new Error(`Unknown provider type: ${conn.providerType}`);
    }
  }
}
