/**
 * Upstash Redis Generator
 * Generates production-ready Upstash Redis client with cache helpers,
 * rate limiting, and session storage utilities
 */

import { Generator, GeneratorContext, GenerationResult, GeneratedFile } from '../types';
import * as path from 'path';

export class UpstashRedisGenerator implements Generator {
  id = 'upstash-redis';
  name = 'Upstash Redis Generator';
  description = 'Generates Upstash Redis client with cache, rate limiting, and session utilities';

  generate(context: GeneratorContext): GenerationResult {
    const { workflow, extendedWorkflow, outputDirectory } = context;
    const files: GeneratedFile[] = [];

    // Check if Redis/cache is configured in extended workflow or legacy workflow
    const hasRedis = this.hasRedisConfiguration(workflow, extendedWorkflow);

    if (!hasRedis) {
      return {
        files: [],
        warnings: ['No Redis/cache configuration found. Skipping Upstash Redis generation.'],
      };
    }

    // Determine backend output directory
    const backendDir = this.getBackendDirectory(workflow, extendedWorkflow);

    // Generate core Redis client
    files.push(this.generateRedisClient(backendDir, outputDirectory));

    // Generate cache helpers
    files.push(this.generateCacheHelpers(backendDir, outputDirectory));

    // Generate rate limiter
    files.push(this.generateRateLimiter(backendDir, outputDirectory));

    // Generate session store
    files.push(this.generateSessionStore(backendDir, outputDirectory));

    // Generate types
    files.push(this.generateTypes(backendDir, outputDirectory));

    // Generate package.json additions
    files.push(this.generatePackageJson(backendDir, outputDirectory));

    // Generate .env.example
    files.push(this.generateEnvExample(backendDir, outputDirectory));

    // Generate README
    files.push(this.generateReadme(backendDir, outputDirectory));

    return { files };
  }

  private hasRedisConfiguration(workflow: any, extendedWorkflow: any): boolean {
    // Check extended workflow technology.cache
    if (extendedWorkflow?.technology?.cache) {
      const provider = extendedWorkflow.technology.cache.provider?.toLowerCase() || '';
      if (provider.includes('redis') || provider.includes('upstash')) {
        return true;
      }
    }

    // Check extended workflow infrastructure.cache
    if (extendedWorkflow?.infrastructure?.cache) {
      return true;
    }

    // Check infrastructure.services for cache type
    if (extendedWorkflow?.infrastructure?.services) {
      for (const service of extendedWorkflow.infrastructure.services) {
        if (service.type === 'cache' || service.type === 'redis') {
          return true;
        }
        const provider = service.provider?.toLowerCase() || '';
        if (provider.includes('redis') || provider.includes('upstash')) {
          return true;
        }
      }
    }

    // Check legacy workflow stages for redis services
    if (workflow.stages) {
      for (const stage of Object.values(workflow.stages) as any[]) {
        if (stage.infrastructure?.services?.redis) {
          return true;
        }
        if (stage.infrastructure?.services?.cache) {
          return true;
        }
      }
    }

    // Check stack for redis/cache components
    if (workflow.stack) {
      for (const component of Object.values(workflow.stack) as any[]) {
        if (component.type === 'cache' || component.type === 'redis') {
          return true;
        }
        if (component.services?.redis || component.services?.cache) {
          return true;
        }
      }
    }

    return false;
  }

  private getBackendDirectory(workflow: any, extendedWorkflow?: any): string {
    // Check extended workflow for backend directory
    if (extendedWorkflow?.technology?.backend?.directory) {
      return extendedWorkflow.technology.backend.directory;
    }

    // Find backend component name from stack
    if (workflow.stack) {
      for (const [name, component] of Object.entries(workflow.stack) as [string, any][]) {
        if (component.type === 'backend') {
          // Use directory if specified, otherwise use component name
          return component.directory || name;
        }
      }
    }

    return 'backend'; // Default
  }

  private generateRedisClient(backendDir: string, outputDir: string): GeneratedFile {
    const content = `/**
 * Upstash Redis Client
 * Production-ready Redis client using Upstash REST API
 * Generated by Bangstack
 */

import { Redis } from '@upstash/redis';

let redisClient: Redis | null = null;

/**
 * Initialize Redis client
 * Call this once during application startup
 */
export function initializeRedis(): Redis {
  if (redisClient) {
    return redisClient;
  }

  const url = process.env.UPSTASH_REDIS_REST_URL;
  const token = process.env.UPSTASH_REDIS_REST_TOKEN;

  if (!url || !token) {
    throw new Error(
      'Missing Upstash Redis credentials. Set UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN environment variables.'
    );
  }

  redisClient = new Redis({
    url,
    token,
  });

  return redisClient;
}

/**
 * Get the Redis client instance
 * Throws if not initialized
 */
export function getRedis(): Redis {
  if (!redisClient) {
    throw new Error('Redis client not initialized. Call initializeRedis() first.');
  }
  return redisClient;
}

/**
 * Health check for Redis connection
 */
export async function redisHealthCheck(): Promise<boolean> {
  try {
    if (!redisClient) {
      return false;
    }
    await redisClient.ping();
    return true;
  } catch (error) {
    console.error('Redis health check failed:', error);
    return false;
  }
}

/**
 * Gracefully close Redis connection
 */
export async function closeRedis(): Promise<void> {
  if (redisClient) {
    // Upstash REST API doesn't require explicit connection closing
    redisClient = null;
  }
}
`;

    return {
      path: path.join(outputDir, backendDir, 'src', 'cache', 'redis-client.ts'),
      content,
      overwrite: true,
    };
  }

  private generateCacheHelpers(backendDir: string, outputDir: string): GeneratedFile {
    const content = `/**
 * Cache Helper Utilities
 * Convenient wrappers for common caching patterns
 * Generated by Bangstack
 */

import { getRedis } from './redis-client';

export interface CacheOptions {
  /** Time to live in seconds */
  ttl?: number;
  /** Cache key prefix */
  prefix?: string;
}

/**
 * Cache Manager
 * Provides type-safe caching with automatic serialization
 */
export class CacheManager {
  private redis = getRedis();
  private prefix: string;

  constructor(prefix: string = 'cache') {
    this.prefix = prefix;
  }

  /**
   * Build cache key with prefix
   */
  private buildKey(key: string): string {
    return \`\${this.prefix}:\${key}\`;
  }

  /**
   * Get cached value
   */
  async get<T>(key: string): Promise<T | null> {
    try {
      const value = await this.redis.get<string>(this.buildKey(key));
      if (value === null) {
        return null;
      }
      return JSON.parse(value) as T;
    } catch (error) {
      console.error('Cache get error:', error);
      return null;
    }
  }

  /**
   * Set cached value with optional TTL
   */
  async set<T>(key: string, value: T, options?: CacheOptions): Promise<boolean> {
    try {
      const serialized = JSON.stringify(value);
      const cacheKey = this.buildKey(key);

      if (options?.ttl) {
        await this.redis.setex(cacheKey, options.ttl, serialized);
      } else {
        await this.redis.set(cacheKey, serialized);
      }

      return true;
    } catch (error) {
      console.error('Cache set error:', error);
      return false;
    }
  }

  /**
   * Delete cached value
   */
  async del(key: string): Promise<boolean> {
    try {
      await this.redis.del(this.buildKey(key));
      return true;
    } catch (error) {
      console.error('Cache delete error:', error);
      return false;
    }
  }

  /**
   * Check if key exists
   */
  async has(key: string): Promise<boolean> {
    try {
      const exists = await this.redis.exists(this.buildKey(key));
      return exists === 1;
    } catch (error) {
      console.error('Cache exists error:', error);
      return false;
    }
  }

  /**
   * Get or set pattern
   * If cache miss, executes factory function and caches result
   */
  async getOrSet<T>(
    key: string,
    factory: () => Promise<T>,
    options?: CacheOptions
  ): Promise<T> {
    const cached = await this.get<T>(key);
    if (cached !== null) {
      return cached;
    }

    const value = await factory();
    await this.set(key, value, options);
    return value;
  }

  /**
   * Clear all keys with this cache manager's prefix
   */
  async clear(): Promise<void> {
    try {
      const pattern = \`\${this.prefix}:*\`;
      const keys = await this.redis.keys(pattern);

      if (keys.length > 0) {
        await this.redis.del(...keys);
      }
    } catch (error) {
      console.error('Cache clear error:', error);
    }
  }

  /**
   * Increment a counter
   */
  async increment(key: string, amount: number = 1): Promise<number> {
    try {
      return await this.redis.incrby(this.buildKey(key), amount);
    } catch (error) {
      console.error('Cache increment error:', error);
      return 0;
    }
  }

  /**
   * Decrement a counter
   */
  async decrement(key: string, amount: number = 1): Promise<number> {
    try {
      return await this.redis.decrby(this.buildKey(key), amount);
    } catch (error) {
      console.error('Cache decrement error:', error);
      return 0;
    }
  }
}

/**
 * Default cache manager instance
 */
export const cache = new CacheManager('app');

/**
 * Decorator for caching method results
 * Usage: @Cached('user', 300)
 */
export function Cached(keyPrefix: string, ttl?: number) {
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    const originalMethod = descriptor.value;

    descriptor.value = async function (...args: any[]) {
      const cacheKey = \`\${keyPrefix}:\${JSON.stringify(args)}\`;
      const cached = await cache.get(cacheKey);

      if (cached !== null) {
        return cached;
      }

      const result = await originalMethod.apply(this, args);
      await cache.set(cacheKey, result, { ttl });
      return result;
    };

    return descriptor;
  };
}
`;

    return {
      path: path.join(outputDir, backendDir, 'src', 'cache', 'cache-helpers.ts'),
      content,
      overwrite: true,
    };
  }

  private generateRateLimiter(backendDir: string, outputDir: string): GeneratedFile {
    const content = `/**
 * Rate Limiter
 * Redis-backed rate limiting with sliding window algorithm
 * Generated by Bangstack
 */

import { getRedis } from './redis-client';
import { Request, Response, NextFunction } from 'express';

export interface RateLimitConfig {
  /** Max requests allowed in the window */
  max: number;
  /** Window duration in seconds */
  window: number;
  /** Key to identify the client (e.g., IP, user ID) */
  keyGenerator?: (req: Request) => string;
  /** Custom handler for rate limit exceeded */
  onLimitExceeded?: (req: Request, res: Response) => void;
}

/**
 * Rate Limiter using sliding window algorithm
 */
export class RateLimiter {
  private redis = getRedis();
  private config: Required<RateLimitConfig>;

  constructor(config: RateLimitConfig) {
    this.config = {
      max: config.max,
      window: config.window,
      keyGenerator: config.keyGenerator || ((req) => req.ip || 'unknown'),
      onLimitExceeded:
        config.onLimitExceeded ||
        ((_req, res) => {
          res.status(429).json({
            error: 'Too many requests',
            message: \`Rate limit exceeded. Max \${config.max} requests per \${config.window} seconds.\`,
          });
        }),
    };
  }

  /**
   * Check if request is within rate limit
   * Returns true if allowed, false if rate limit exceeded
   */
  async isAllowed(identifier: string): Promise<{
    allowed: boolean;
    remaining: number;
    resetAt: Date;
  }> {
    const key = \`ratelimit:\${identifier}\`;
    const now = Date.now();
    const windowStart = now - this.config.window * 1000;

    try {
      // Remove old entries outside the window
      await this.redis.zremrangebyscore(key, 0, windowStart);

      // Count requests in current window
      const count = await this.redis.zcount(key, windowStart, now);

      if (count >= this.config.max) {
        // Get oldest request time to calculate reset
        const oldest = await this.redis.zrange(key, 0, 0, { withScores: true });
        const resetAt = new Date(
          (oldest[1] as number) + this.config.window * 1000
        );

        return {
          allowed: false,
          remaining: 0,
          resetAt,
        };
      }

      // Add current request
      await this.redis.zadd(key, { score: now, member: \`\${now}\` });
      await this.redis.expire(key, this.config.window);

      return {
        allowed: true,
        remaining: this.config.max - (count + 1),
        resetAt: new Date(now + this.config.window * 1000),
      };
    } catch (error) {
      console.error('Rate limiter error:', error);
      // Fail open - allow request on error
      return {
        allowed: true,
        remaining: this.config.max,
        resetAt: new Date(now + this.config.window * 1000),
      };
    }
  }

  /**
   * Express middleware for rate limiting
   */
  middleware() {
    return async (req: Request, res: Response, next: NextFunction) => {
      const identifier = this.config.keyGenerator(req);
      const result = await this.isAllowed(identifier);

      // Set rate limit headers
      res.setHeader('X-RateLimit-Limit', this.config.max);
      res.setHeader('X-RateLimit-Remaining', result.remaining);
      res.setHeader('X-RateLimit-Reset', result.resetAt.toISOString());

      if (!result.allowed) {
        this.config.onLimitExceeded(req, res);
        return;
      }

      next();
    };
  }
}

/**
 * Create a rate limiter with default settings
 */
export function createRateLimiter(
  max: number,
  windowSeconds: number,
  options?: Partial<RateLimitConfig>
): RateLimiter {
  return new RateLimiter({
    max,
    window: windowSeconds,
    ...options,
  });
}

/**
 * Common rate limit presets
 */
export const RateLimitPresets = {
  /** 100 requests per minute */
  standard: () => createRateLimiter(100, 60),

  /** 10 requests per minute */
  strict: () => createRateLimiter(10, 60),

  /** 1000 requests per minute */
  lenient: () => createRateLimiter(1000, 60),

  /** 5 requests per 15 minutes (for auth endpoints) */
  auth: () => createRateLimiter(5, 900),
};
`;

    return {
      path: path.join(outputDir, backendDir, 'src', 'cache', 'rate-limiter.ts'),
      content,
      overwrite: true,
    };
  }

  private generateSessionStore(backendDir: string, outputDir: string): GeneratedFile {
    const content = `/**
 * Redis Session Store
 * Express session store backed by Upstash Redis
 * Generated by Bangstack
 */

import { getRedis } from './redis-client';
import { Store } from 'express-session';

export interface SessionData {
  cookie: any;
  [key: string]: any;
}

/**
 * Redis Session Store for express-session
 */
export class RedisSessionStore extends Store {
  private redis = getRedis();
  private prefix: string;
  private ttl: number;

  constructor(options: { prefix?: string; ttl?: number } = {}) {
    super();
    this.prefix = options.prefix || 'sess:';
    this.ttl = options.ttl || 86400; // Default 24 hours
  }

  /**
   * Build session key
   */
  private getKey(sid: string): string {
    return \`\${this.prefix}\${sid}\`;
  }

  /**
   * Get session
   */
  get(
    sid: string,
    callback: (err: any, session?: SessionData | null) => void
  ): void {
    this.redis
      .get<string>(this.getKey(sid))
      .then((data) => {
        if (!data) {
          callback(null, null);
          return;
        }
        callback(null, JSON.parse(data));
      })
      .catch((err) => callback(err));
  }

  /**
   * Set session
   */
  set(sid: string, session: SessionData, callback?: (err?: any) => void): void {
    try {
      const ttl = this.getTTL(session);
      const serialized = JSON.stringify(session);

      this.redis
        .setex(this.getKey(sid), ttl, serialized)
        .then(() => callback?.())
        .catch((err) => callback?.(err));
    } catch (err) {
      callback?.(err);
    }
  }

  /**
   * Destroy session
   */
  destroy(sid: string, callback?: (err?: any) => void): void {
    this.redis
      .del(this.getKey(sid))
      .then(() => callback?.())
      .catch((err) => callback?.(err));
  }

  /**
   * Touch session (update expiration)
   */
  touch(sid: string, session: SessionData, callback?: (err?: any) => void): void {
    const ttl = this.getTTL(session);

    this.redis
      .expire(this.getKey(sid), ttl)
      .then(() => callback?.())
      .catch((err) => callback?.(err));
  }

  /**
   * Calculate TTL from session cookie
   */
  private getTTL(session: SessionData): number {
    if (session.cookie && session.cookie.maxAge) {
      return Math.floor(session.cookie.maxAge / 1000);
    }
    return this.ttl;
  }
}

/**
 * Create a Redis session store
 */
export function createSessionStore(options?: {
  prefix?: string;
  ttl?: number;
}): RedisSessionStore {
  return new RedisSessionStore(options);
}
`;

    return {
      path: path.join(outputDir, backendDir, 'src', 'cache', 'session-store.ts'),
      content,
      overwrite: true,
    };
  }

  private generateTypes(backendDir: string, outputDir: string): GeneratedFile {
    const content = `/**
 * Redis & Cache Type Definitions
 * Generated by Bangstack
 */

export interface RedisConfig {
  url: string;
  token: string;
}

export interface CacheEntry<T> {
  value: T;
  expiresAt?: number;
}

export interface RateLimitResult {
  allowed: boolean;
  remaining: number;
  resetAt: Date;
}
`;

    return {
      path: path.join(outputDir, backendDir, 'src', 'cache', 'types.ts'),
      content,
      overwrite: true,
    };
  }

  private generatePackageJson(backendDir: string, outputDir: string): GeneratedFile {
    const pkg = {
      name: `${backendDir}-redis`,
      version: '1.0.0',
      description: 'Upstash Redis integration generated by Bangstack',
      dependencies: {
        '@upstash/redis': '^1.28.0',
        'express-session': '^1.17.3',
      },
      devDependencies: {
        '@types/express-session': '^1.17.10',
      },
    };

    return {
      path: path.join(outputDir, backendDir, 'cache-package.json'),
      content: JSON.stringify(pkg, null, 2),
      overwrite: false,
    };
  }

  private generateEnvExample(backendDir: string, outputDir: string): GeneratedFile {
    const content = `# Upstash Redis Configuration
# Get these values from https://console.upstash.com/

UPSTASH_REDIS_REST_URL=https://your-redis-url.upstash.io
UPSTASH_REDIS_REST_TOKEN=your-redis-token-here
`;

    return {
      path: path.join(outputDir, backendDir, '.env.redis.example'),
      content,
      overwrite: false,
    };
  }

  private generateReadme(backendDir: string, outputDir: string): GeneratedFile {
    const content = `# Upstash Redis Integration

Generated by Bangstack - Production-ready Redis client with utilities.

## Features

- ✅ **Redis Client**: Upstash REST API integration
- ✅ **Cache Helpers**: Type-safe caching with TTL support
- ✅ **Rate Limiting**: Sliding window rate limiter
- ✅ **Session Store**: Express session store backed by Redis
- ✅ **Health Checks**: Built-in connection monitoring

## Setup

### 1. Install Dependencies

\`\`\`bash
npm install @upstash/redis express-session
npm install -D @types/express-session
\`\`\`

### 2. Get Upstash Credentials

1. Go to [https://console.upstash.com/](https://console.upstash.com/)
2. Create a new Redis database (or use existing)
3. Copy the REST URL and REST Token

### 3. Configure Environment

\`\`\`bash
cp .env.redis.example .env.redis
# Edit .env.redis with your Upstash credentials
\`\`\`

Add to your main \`.env\`:
\`\`\`
UPSTASH_REDIS_REST_URL=https://your-redis-url.upstash.io
UPSTASH_REDIS_REST_TOKEN=your-token-here
\`\`\`

### 4. Initialize in Your App

\`\`\`typescript
import { initializeRedis } from './cache/redis-client';

// Initialize at app startup
initializeRedis();
\`\`\`

## Usage

### Cache Helpers

\`\`\`typescript
import { cache } from './cache/cache-helpers';

// Set with TTL
await cache.set('user:123', userData, { ttl: 3600 });

// Get
const user = await cache.get<User>('user:123');

// Get or set pattern
const data = await cache.getOrSet('expensive-query', async () => {
  return await database.expensiveQuery();
}, { ttl: 600 });

// Delete
await cache.del('user:123');
\`\`\`

### Rate Limiting

\`\`\`typescript
import { RateLimitPresets } from './cache/rate-limiter';
import express from 'express';

const app = express();

// Apply rate limiting to all routes
app.use(RateLimitPresets.standard().middleware());

// Custom rate limit for auth endpoints
app.post('/auth/login',
  RateLimitPresets.auth().middleware(),
  loginHandler
);
\`\`\`

### Session Store

\`\`\`typescript
import session from 'express-session';
import { createSessionStore } from './cache/session-store';

app.use(session({
  store: createSessionStore({ ttl: 86400 }),
  secret: process.env.SESSION_SECRET!,
  resave: false,
  saveUninitialized: false,
}));
\`\`\`

## API Reference

### CacheManager

- \`get<T>(key)\` - Get cached value
- \`set<T>(key, value, options)\` - Set with optional TTL
- \`del(key)\` - Delete cached value
- \`has(key)\` - Check if key exists
- \`getOrSet<T>(key, factory, options)\` - Get or compute and cache
- \`clear()\` - Clear all keys with prefix
- \`increment(key, amount)\` - Increment counter
- \`decrement(key, amount)\` - Decrement counter

### RateLimiter

- \`isAllowed(identifier)\` - Check if request is allowed
- \`middleware()\` - Express middleware

### RedisSessionStore

Compatible with express-session. See session store setup above.

## Health Checks

\`\`\`typescript
import { redisHealthCheck } from './cache/redis-client';

app.get('/health', async (req, res) => {
  const redisHealthy = await redisHealthCheck();
  res.json({
    status: redisHealthy ? 'healthy' : 'unhealthy',
    redis: redisHealthy
  });
});
\`\`\`

## Documentation

- [Upstash Redis Docs](https://docs.upstash.com/redis)
- [Upstash Node.js SDK](https://github.com/upstash/redis-js)

## Generated by Bangstack

This Redis integration was automatically generated. You can safely modify
these files to fit your needs.
`;

    return {
      path: path.join(outputDir, backendDir, 'REDIS.md'),
      content,
      overwrite: false,
    };
  }
}
