import * as plugins from './smartproxy.plugins.js';


export interface DomainConfig {
  domain: string;  // glob pattern for domain
  allowedIPs: string[];  // glob patterns for IPs allowed to access this domain
}

export interface ProxySettings extends plugins.tls.TlsOptions {
  // Port configuration
  fromPort: number;
  toPort: number;
  toHost?: string;  // Target host to proxy to, defaults to 'localhost'

  // Domain and security settings
  domains: DomainConfig[];
  sniEnabled?: boolean;
  defaultAllowedIPs?: string[];  // Optional default IP patterns if no matching domain found
}

export class PortProxy {
  netServer: plugins.net.Server | plugins.tls.Server;
  settings: ProxySettings;

  constructor(settings: ProxySettings) {
    this.settings = {
      ...settings,
      toHost: settings.toHost || 'localhost'
    };
  }

  public async start() {
    const cleanUpSockets = (from: plugins.net.Socket, to: plugins.net.Socket) => {
      from.end();
      to.end();
      from.removeAllListeners();
      to.removeAllListeners();
      from.unpipe();
      to.unpipe();
      from.destroy();
      to.destroy();
    };
    const normalizeIP = (ip: string): string[] => {
      // Handle IPv4-mapped IPv6 addresses
      if (ip.startsWith('::ffff:')) {
        const ipv4 = ip.slice(7); // Remove '::ffff:' prefix
        return [ip, ipv4];
      }
      // Handle IPv4 addresses by adding IPv4-mapped IPv6 variant
      if (ip.match(/^\d{1,3}(\.\d{1,3}){3}$/)) {
        return [ip, `::ffff:${ip}`];
      }
      return [ip];
    };

    const isAllowed = (value: string, patterns: string[]): boolean => {
      // Expand patterns to include both IPv4 and IPv6 variants
      const expandedPatterns = patterns.flatMap(normalizeIP);
      // Check if any variant of the IP matches any expanded pattern
      return normalizeIP(value).some(ip => 
        expandedPatterns.some(pattern => plugins.minimatch(ip, pattern))
      );
    };

    const findMatchingDomain = (serverName: string): DomainConfig | undefined => {
      return this.settings.domains.find(config => plugins.minimatch(serverName, config.domain));
    };

    const server = this.settings.sniEnabled
      ? plugins.tls.createServer(this.settings)
      : plugins.net.createServer();
    
    this.netServer = server.on('connection', (from: plugins.net.Socket) => {
      const remoteIP = from.remoteAddress || '';
        if (this.settings.sniEnabled && from instanceof plugins.tls.TLSSocket) {
          const serverName = (from as any).servername || '';
          const domainConfig = findMatchingDomain(serverName);

          if (!domainConfig) {
            // If no matching domain config found, check default IPs if available
            if (!this.settings.defaultAllowedIPs || !isAllowed(remoteIP, this.settings.defaultAllowedIPs)) {
              console.log(`Connection rejected: No matching domain config for ${serverName} from IP ${remoteIP}`);
              from.end();
              return;
            }
          } else {
            // Check if IP is allowed for this domain
            if (!isAllowed(remoteIP, domainConfig.allowedIPs)) {
              console.log(`Connection rejected: IP ${remoteIP} not allowed for domain ${serverName}`);
              from.end();
              return;
            }
          }
        } else if (!this.settings.defaultAllowedIPs || !isAllowed(remoteIP, this.settings.defaultAllowedIPs)) {
          console.log(`Connection rejected: IP ${remoteIP} not allowed for non-SNI connection`);
          from.end();
          return;
        }

        const to = plugins.net.createConnection({
          host: this.settings.toHost!,
          port: this.settings.toPort,
        });
        console.log(`Connection established: ${remoteIP} -> ${this.settings.toHost}:${this.settings.toPort}${this.settings.sniEnabled ? ` (SNI: ${(from as any).servername || 'none'})` : ''}`);
        from.setTimeout(120000);
        from.pipe(to);
        to.pipe(from);
        from.on('error', () => {
          cleanUpSockets(from, to);
        });
        to.on('error', () => {
          cleanUpSockets(from, to);
        });
        from.on('close', () => {
          cleanUpSockets(from, to);
        });
        to.on('close', () => {
          cleanUpSockets(from, to);
        });
        from.on('timeout', () => {
          cleanUpSockets(from, to);
        });
        to.on('timeout', () => {
          cleanUpSockets(from, to);
        });
        from.on('end', () => {
          cleanUpSockets(from, to);
        });
        to.on('end', () => {
          cleanUpSockets(from, to);
        });
      })
      .listen(this.settings.fromPort);
    console.log(`PortProxy -> OK: Now listening on port ${this.settings.fromPort}`);
  }

  public async stop() {
    const done = plugins.smartpromise.defer();
    this.netServer.close(() => {
      done.resolve();
    });
    await done.promise;
  }
}