import * as fs from 'fs/promises';
import * as path from 'path';
import { AuthenticationState } from '../../../../domain/value-objects/AuthenticationState';
import { CookieConfiguration } from '../../../../domain/value-objects/CookieConfiguration';
import { Result } from '@gridpilot/shared/application/Result';
import type { LoggerPort } from '../../../../application/ports/LoggerPort';

interface Cookie {
  name: string;
  value: string;
  domain: string;
  path: string;
  expires: number;
}

interface StorageState {
  cookies: Cookie[];
  origins: unknown[];
}

/**
 * Known iRacing session cookie names to look for.
 * These are the primary authentication indicators.
 */
const IRACING_SESSION_COOKIES = [
  'irsso_members',
  'authtoken_members',
  'irsso',
  'authtoken',
];

/**
 * iRacing domain patterns to match cookies against.
 */
const IRACING_DOMAINS = [
  'iracing.com',
  '.iracing.com',
  'members.iracing.com',
  'members-ng.iracing.com',
];

const EXPIRY_BUFFER_SECONDS = 300;

export class SessionCookieStore {
  private readonly storagePath: string;
  private readonly logger: LoggerPort | undefined;
 
  constructor(userDataDir: string, logger: LoggerPort | undefined) {
    this.storagePath = path.join(userDataDir, 'session-state.json');
    this.logger = logger;
  }

  private log(level: 'debug' | 'info' | 'warn' | 'error', message: string, context?: Record<string, unknown>): void {
    if (this.logger) {
      if (level === 'error') {
        this.logger.error(message, undefined, context);
      } else {
        this.logger[level](message, context);
      }
    }
  }

  getPath(): string {
    return this.storagePath;
  }

  async read(): Promise<StorageState | null> {
    try {
      const content = await fs.readFile(this.storagePath, 'utf-8');
      const state = JSON.parse(content) as StorageState;
      
      // Ensure all cookies have path field (default to "/" for backward compatibility)
      state.cookies = state.cookies.map(cookie => ({
        ...cookie,
        path: cookie.path || '/'
      }));
      
      this.cachedState = state;
      return state;
    } catch {
      return null;
    }
  }

  async write(state: StorageState): Promise<void> {
    this.cachedState = state;
    await fs.writeFile(this.storagePath, JSON.stringify(state, null, 2), 'utf-8');
  }

  async delete(): Promise<void> {
    try {
      await fs.unlink(this.storagePath);
    } catch {
      // File may not exist, ignore
    }
  }

  /**
   * Get session expiry date from iRacing cookies.
   * Returns the earliest expiry date from valid session cookies.
   */
  async getSessionExpiry(): Promise<Date | null> {
    try {
      const state = await this.read();
      if (!state || state.cookies.length === 0) {
        return null;
      }

      // Filter to iRacing authentication cookies
      const authCookies = state.cookies.filter(c =>
        IRACING_DOMAINS.some(domain =>
          c.domain === domain || c.domain.endsWith(domain)
        ) &&
        (IRACING_SESSION_COOKIES.some(name =>
          c.name.toLowerCase().includes(name.toLowerCase())
        ) ||
        c.name.toLowerCase().includes('auth') ||
        c.name.toLowerCase().includes('sso') ||
        c.name.toLowerCase().includes('token'))
      );

      if (authCookies.length === 0) {
        return null;
      }

      // Find the earliest expiry date (most restrictive)
      // Session cookies (expires = -1 or 0) are treated as never expiring
      const expiryDates = authCookies
        .filter(c => c.expires > 0)
        .map(c => {
          // Handle both formats: seconds (standard) and milliseconds (test fixtures)
          // If expires > year 2100 in seconds (33134745600), it's likely milliseconds
          const isMilliseconds = c.expires > 33134745600;
          return new Date(isMilliseconds ? c.expires : c.expires * 1000);
        });

      if (expiryDates.length === 0) {
        // All session cookies, no expiry
        return null;
      }

      // Return earliest expiry
      const earliestExpiry = new Date(Math.min(...expiryDates.map(d => d.getTime())));
      
      this.log('debug', 'Session expiry determined', {
        earliestExpiry: earliestExpiry.toISOString(),
        cookiesChecked: authCookies.length
      });

      return earliestExpiry;
    } catch (error) {
      this.log('error', 'Failed to get session expiry', { error: String(error) });
      return null;
    }
  }

  /**
   * Validate cookies and determine authentication state.
   *
   * Looks for iRacing session cookies by checking:
   * 1. Domain matches iRacing patterns
   * 2. Cookie name matches known session cookie names OR
   * 3. Any cookie from members.iracing.com domain (fallback)
   */
  validateCookies(cookies: Cookie[]): AuthenticationState {
    // Log all cookies for debugging
    this.log('debug', 'Validating cookies', {
      totalCookies: cookies.length,
      cookieNames: cookies.map(c => ({ name: c.name, domain: c.domain }))
    });

    // Filter cookies from iRacing domains
    const iracingDomainCookies = cookies.filter(c =>
      IRACING_DOMAINS.some(domain =>
        c.domain === domain || c.domain.endsWith(domain)
      )
    );

    this.log('debug', 'iRacing domain cookies found', {
      count: iracingDomainCookies.length,
      cookies: iracingDomainCookies.map(c => ({
        name: c.name,
        domain: c.domain,
        expires: c.expires,
        expiresDate: new Date(c.expires * 1000).toISOString()
      }))
    });

    // Look for known session cookies first
    const knownSessionCookies = iracingDomainCookies.filter(c =>
      IRACING_SESSION_COOKIES.some(name =>
        c.name.toLowerCase() === name.toLowerCase() ||
        c.name.toLowerCase().includes(name.toLowerCase())
      )
    );

    // If no known session cookies, check for any auth-like cookies from members domain
    const authCookies = knownSessionCookies.length > 0
      ? knownSessionCookies
      : iracingDomainCookies.filter(c =>
          c.domain.includes('members') &&
          (c.name.toLowerCase().includes('auth') ||
           c.name.toLowerCase().includes('sso') ||
           c.name.toLowerCase().includes('session') ||
           c.name.toLowerCase().includes('token'))
        );

    this.log('debug', 'Authentication cookies identified', {
      knownSessionCookiesCount: knownSessionCookies.length,
      authCookiesCount: authCookies.length,
      cookies: authCookies.map(c => ({ name: c.name, domain: c.domain }))
    });

    if (authCookies.length === 0) {
      // Last resort: if we have ANY cookies from members.iracing.com, consider it potentially valid
      const membersCookies = iracingDomainCookies.filter(c =>
        c.domain.includes('members.iracing.com') || c.domain === '.iracing.com'
      );
      
      if (membersCookies.length > 0) {
        this.log('info', 'No known auth cookies found, but members domain cookies exist', {
          count: membersCookies.length,
          cookies: membersCookies.map(c => c.name)
        });
        
        // Check expiry on any of these cookies
        const now = Math.floor(Date.now() / 1000);
        const hasValidCookie = membersCookies.some(c =>
          c.expires === -1 || c.expires === 0 || c.expires > (now + EXPIRY_BUFFER_SECONDS)
        );
        
        return hasValidCookie
          ? AuthenticationState.AUTHENTICATED
          : AuthenticationState.EXPIRED;
      }
      
      this.log('info', 'No iRacing authentication cookies found');
      return AuthenticationState.UNKNOWN;
    }

    // Check if any auth cookie is still valid (not expired)
    const now = Math.floor(Date.now() / 1000);
    const validCookies = authCookies.filter(c => {
      // Handle session cookies (expires = -1 or 0) and persistent cookies
      const isSession = c.expires === -1 || c.expires === 0;
      const isNotExpired = c.expires > (now + EXPIRY_BUFFER_SECONDS);
      return isSession || isNotExpired;
    });

    this.log('debug', 'Cookie expiry check', {
      now,
      validCookiesCount: validCookies.length,
      cookies: authCookies.map(c => ({
        name: c.name,
        expires: c.expires,
        isValid: c.expires === -1 || c.expires === 0 || c.expires > (now + EXPIRY_BUFFER_SECONDS)
      }))
    });

    if (validCookies.length > 0) {
      this.log('info', 'Valid iRacing session cookies found', { count: validCookies.length });
      return AuthenticationState.AUTHENTICATED;
    }

    this.log('info', 'iRacing session cookies found but all expired');
    return AuthenticationState.EXPIRED;
  }

  private cachedState: StorageState | null = null;

  /**
   * Validate stored cookies for a target URL.
   * Note: This requires cookies to be written first via write().
   * This is synchronous because tests expect it - uses cached state.
   * Validates domain/path compatibility AND checks for required authentication cookies.
   */
  validateCookieConfiguration(targetUrl: string): Result<Cookie[]> {
    try {
      if (!this.cachedState || this.cachedState.cookies.length === 0) {
        return Result.err<Cookie[]>(new Error('No cookies found in session store'));
      }

      return this.validateCookiesForUrl(this.cachedState.cookies, targetUrl, true);
    } catch (error) {
      const message = error instanceof Error ? error.message : String(error);
      return Result.err<Cookie[]>(new Error(`Cookie validation failed: ${message}`));
    }
  }

  /**
   * Validate a list of cookies for a target URL.
   * Returns only cookies that are valid for the target URL.
   * @param requireAuthCookies - If true, checks for required authentication cookies
   */
  validateCookiesForUrl(
    cookies: Cookie[],
    targetUrl: string,
    requireAuthCookies = false
  ): Result<Cookie[]> {
    try {
      const validatedCookies: Cookie[] = [];
      let firstValidationError: Error | null = null;

      for (const cookie of cookies) {
        try {
          new CookieConfiguration(cookie, targetUrl);
          validatedCookies.push(cookie);
        } catch (error) {
          const err = error instanceof Error ? error : new Error(String(error));

          if (!firstValidationError) {
            firstValidationError = err;
          }

          this.logger?.warn('Cookie validation failed', {
            name: cookie.name,
            error: err.message,
          });
        }
      }

      if (validatedCookies.length === 0) {
        return Result.err<Cookie[]>(
          firstValidationError ?? new Error('No valid cookies found for target URL')
        );
      }

      if (requireAuthCookies) {
        const cookieNames = validatedCookies.map((c) => c.name.toLowerCase());

        const hasIrssoMembers = cookieNames.some((name) =>
          name.includes('irsso_members') || name.includes('irsso')
        );

        const hasAuthtokenMembers = cookieNames.some((name) =>
          name.includes('authtoken_members') || name.includes('authtoken')
        );

        if (!hasIrssoMembers) {
          return Result.err<Cookie[]>(new Error('Required cookie missing: irsso_members'));
        }

        if (!hasAuthtokenMembers) {
          return Result.err<Cookie[]>(new Error('Required cookie missing: authtoken_members'));
        }
      }

      return Result.ok<Cookie[]>(validatedCookies);
    } catch (error) {
      const err = error instanceof Error ? error : new Error(String(error));
      return Result.err<Cookie[]>(new Error(`Cookie validation failed: ${err.message}`));
    }
  }

  /**
   * Get cookies that are valid for a target URL.
   * Returns array of cookies (empty if none valid).
   * Uses cached state from last write().
   */
  getValidCookiesForUrl(targetUrl: string): Cookie[] {
    try {
      if (!this.cachedState || this.cachedState.cookies.length === 0) {
        return [];
      }

      const result = this.validateCookiesForUrl(this.cachedState.cookies, targetUrl);
      return result.isOk() ? result.unwrap() : [];
    } catch {
      return [];
    }
  }
}