/**
 * Japan Post Connection Service
 *
 * HTTP client for Japan Post Digital Address API with OAuth token management.
 *
 * Required Environment Variables:
 *   JAPAN_POST_API_URL       - Base URL for Japan Post Digital Address API
 *   JAPAN_POST_CLIENT_ID     - OAuth client ID
 *   JAPAN_POST_CLIENT_SECRET - OAuth client secret
 *
 * Optional Environment Variables:
 *   JAPAN_POST_TIMEOUT       - Request timeout in ms (default: 10000)
 */

import { Injectable, Inject, type OnModuleInit } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { Logger } from "nestjs-pino";
import { extractErrorMessage } from "@bff/core/utils/error.util.js";
import {
  japanPostTokenResponseSchema,
  type JapanPostTokenResponse,
} from "@customer-portal/domain/address/providers";

interface JapanPostConfig {
  baseUrl: string;
  clientId: string;
  clientSecret: string;
  timeout: number;
}

interface ConfigValidationError {
  envVar: string;
  message: string;
}

/**
 * Japan Post API error response format
 */
interface JapanPostErrorResponse {
  request_id?: string | undefined;
  error_code?: string | undefined;
  message?: string | undefined;
}

@Injectable()
export class JapanPostConnectionService implements OnModuleInit {
  private accessToken: string | null = null;
  private tokenExpiresAt: number = 0;
  private readonly config: JapanPostConfig;
  private readonly configErrors: ConfigValidationError[];

  constructor(
    private readonly configService: ConfigService,
    @Inject(Logger) private readonly logger: Logger
  ) {
    this.config = {
      baseUrl: this.configService.get<string>("JAPAN_POST_API_URL") || "",
      clientId: this.configService.get<string>("JAPAN_POST_CLIENT_ID") || "",
      clientSecret: this.configService.get<string>("JAPAN_POST_CLIENT_SECRET") || "",
      timeout: this.configService.get<number>("JAPAN_POST_TIMEOUT") || 10000,
    };

    // Validate configuration
    this.configErrors = this.validateConfig();
  }

  /**
   * Validate required environment variables
   */
  private validateConfig(): ConfigValidationError[] {
    const errors: ConfigValidationError[] = [];

    if (!this.config.baseUrl) {
      errors.push({
        envVar: "JAPAN_POST_API_URL",
        message: "Missing required environment variable JAPAN_POST_API_URL",
      });
    } else if (!this.config.baseUrl.startsWith("https://")) {
      errors.push({
        envVar: "JAPAN_POST_API_URL",
        message: "JAPAN_POST_API_URL must use HTTPS",
      });
    }

    if (!this.config.clientId) {
      errors.push({
        envVar: "JAPAN_POST_CLIENT_ID",
        message: "Missing required environment variable JAPAN_POST_CLIENT_ID",
      });
    }

    if (!this.config.clientSecret) {
      errors.push({
        envVar: "JAPAN_POST_CLIENT_SECRET",
        message: "Missing required environment variable JAPAN_POST_CLIENT_SECRET",
      });
    }

    if (this.config.timeout < 1000 || this.config.timeout > 60000) {
      errors.push({
        envVar: "JAPAN_POST_TIMEOUT",
        message: "JAPAN_POST_TIMEOUT must be between 1000ms and 60000ms",
      });
    }

    return errors;
  }

  /**
   * Log configuration status on module initialization
   */
  onModuleInit() {
    if (this.configErrors.length > 0) {
      this.logger.error(
        "Japan Post API configuration is invalid. Address lookup will be unavailable.",
        {
          errors: this.configErrors,
          hint: "Add the required environment variables to your .env file",
        }
      );
    } else {
      this.logger.log("Japan Post API configured successfully", {
        baseUrl: this.config.baseUrl.replace(/\/+$/, ""),
        timeout: this.config.timeout,
      });
    }
  }

  /**
   * Get configuration validation errors (for health checks)
   */
  getConfigErrors(): ConfigValidationError[] {
    return this.configErrors;
  }

  /**
   * Get a valid access token, refreshing if necessary
   */
  private async getAccessToken(): Promise<string> {
    // Fail fast if not configured
    this.assertConfigured();

    const now = Date.now();

    // Return cached token if still valid (with 60s buffer)
    if (this.accessToken && this.tokenExpiresAt > now + 60000) {
      return this.accessToken;
    }

    this.logger.debug("Acquiring Japan Post access token");

    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
    const startTime = Date.now();
    const tokenUrl = `${this.config.baseUrl}/api/v1/j/token`;

    try {
      const response = await fetch(tokenUrl, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          "x-forwarded-for": "127.0.0.1", // Required by API
        },
        body: JSON.stringify({
          grant_type: "client_credentials",
          client_id: this.config.clientId,
          secret_key: this.config.clientSecret,
        }),
        signal: controller.signal,
      });

      const durationMs = Date.now() - startTime;

      if (!response.ok) {
        const errorBody = await response.text().catch(() => "");
        const parsedError = this.parseErrorResponse(errorBody);

        this.logger.error("Japan Post token request failed", {
          endpoint: tokenUrl,
          httpStatus: response.status,
          httpStatusText: response.statusText,
          durationMs,
          // Japan Post API error details
          requestId: parsedError?.request_id,
          errorCode: parsedError?.error_code,
          apiMessage: parsedError?.message,
          hint: this.getErrorHint(response.status, parsedError?.error_code),
        });
        throw new Error(`Token request failed: HTTP ${response.status}`);
      }

      const data = (await response.json()) as JapanPostTokenResponse;
      const validated = japanPostTokenResponseSchema.parse(data);

      const token = validated.token;
      this.accessToken = token;
      this.tokenExpiresAt = now + validated.expires_in * 1000;

      this.logger.debug("Japan Post token acquired", {
        expiresIn: validated.expires_in,
        tokenType: validated.token_type,
        durationMs,
      });

      return token;
    } catch (error) {
      const durationMs = Date.now() - startTime;
      const isAborted = error instanceof Error && error.name === "AbortError";

      if (isAborted) {
        this.logger.error("Japan Post token request timed out", {
          endpoint: tokenUrl,
          timeoutMs: this.config.timeout,
          durationMs,
        });
        throw new Error(`Token request timed out after ${this.config.timeout}ms`);
      }

      // Only log if not already logged above (non-ok response)
      if (!(error instanceof Error && error.message.startsWith("Token request failed"))) {
        this.logger.error("Japan Post token request error", {
          endpoint: tokenUrl,
          error: extractErrorMessage(error),
          durationMs,
        });
      }
      throw error;
    } finally {
      clearTimeout(timeoutId);
    }
  }

  /**
   * Parse Japan Post API error response
   */
  private parseErrorResponse(body: string): JapanPostErrorResponse | null {
    if (!body) return null;
    try {
      const parsed = JSON.parse(body) as JapanPostErrorResponse;
      // Validate it has the expected shape
      if (parsed && typeof parsed === "object") {
        return {
          request_id: parsed.request_id,
          error_code: parsed.error_code,
          message: parsed.message,
        };
      }
      return null;
    } catch {
      return null;
    }
  }

  /**
   * Get a helpful hint based on HTTP status code and API error code
   */
  private getErrorHint(status: number, errorCode?: string): string {
    // Handle specific Japan Post error codes (format: "400-1028-0001")
    if (errorCode) {
      if (errorCode.startsWith("400-1028")) {
        return "Invalid client_id or secret_key - check JAPAN_POST_CLIENT_ID and JAPAN_POST_CLIENT_SECRET";
      }
      if (errorCode.startsWith("401")) {
        return "Token is invalid or expired - will retry with fresh token";
      }
      if (errorCode.startsWith("404")) {
        return "ZIP code not found in Japan Post database";
      }
    }

    // Fall back to HTTP status hints
    switch (status) {
      case 400:
        return "Invalid request - check ZIP code format (7 digits)";
      case 401:
        return "Token expired or invalid - check credentials";
      case 403:
        return "Access forbidden - API credentials may be suspended";
      case 404:
        return "ZIP code not found in Japan Post database";
      case 429:
        return "Rate limit exceeded - reduce request frequency";
      case 500:
        return "Japan Post API internal error - retry later";
      case 502:
      case 503:
      case 504:
        return "Japan Post API is temporarily unavailable - retry later";
      default:
        return "Unexpected error - check API configuration";
    }
  }

  /**
   * Search addresses by ZIP code
   *
   * @param zipCode - 7-digit ZIP code (no hyphen)
   * @param clientIp - Client IP address for x-forwarded-for header
   * @returns Raw Japan Post API response
   */
  async searchByZipCode(zipCode: string, clientIp: string = "127.0.0.1"): Promise<unknown> {
    const token = await this.getAccessToken();

    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
    const startTime = Date.now();
    const url = `${this.config.baseUrl}/api/v1/searchcode/${zipCode}`;

    this.logger.debug("Japan Post ZIP code search started", { zipCode });

    try {
      const response = await fetch(url, {
        method: "GET",
        headers: {
          Authorization: `Bearer ${token}`,
          "Content-Type": "application/json",
          "x-forwarded-for": clientIp,
        },
        signal: controller.signal,
      });

      const durationMs = Date.now() - startTime;

      if (!response.ok) {
        const errorBody = await response.text().catch(() => "");
        const parsedError = this.parseErrorResponse(errorBody);

        this.logger.error("Japan Post ZIP search request failed", {
          zipCode,
          endpoint: url,
          httpStatus: response.status,
          httpStatusText: response.statusText,
          durationMs,
          // Japan Post API error details
          requestId: parsedError?.request_id,
          errorCode: parsedError?.error_code,
          apiMessage: parsedError?.message,
          hint: this.getErrorHint(response.status, parsedError?.error_code),
        });
        throw new Error(`ZIP code search failed: HTTP ${response.status}`);
      }

      const data = await response.json();

      this.logger.debug("Japan Post ZIP search completed", {
        zipCode,
        resultCount: (data as { count?: number }).count,
        durationMs,
      });

      return data;
    } catch (error) {
      const durationMs = Date.now() - startTime;
      const isAborted = error instanceof Error && error.name === "AbortError";

      if (isAborted) {
        this.logger.error("Japan Post ZIP search timed out", {
          zipCode,
          endpoint: url,
          timeoutMs: this.config.timeout,
          durationMs,
        });
        throw new Error(`ZIP search timed out after ${this.config.timeout}ms`);
      }

      // Only log if not already logged above (non-ok response)
      if (!(error instanceof Error && error.message.startsWith("ZIP code search failed"))) {
        this.logger.error("Japan Post ZIP search error", {
          zipCode,
          endpoint: url,
          error: extractErrorMessage(error),
          durationMs,
        });
      }
      throw error;
    } finally {
      clearTimeout(timeoutId);
    }
  }

  /**
   * Check if the service is properly configured
   */
  isConfigured(): boolean {
    return this.configErrors.length === 0;
  }

  /**
   * Throw an error if configuration is invalid
   */
  private assertConfigured(): void {
    if (this.configErrors.length > 0) {
      const missingVars = this.configErrors.map(e => e.envVar).join(", ");
      throw new Error(`Japan Post API is not configured. Missing or invalid: ${missingVars}`);
    }
  }

  /**
   * Clear cached token (for testing or forced refresh)
   */
  clearTokenCache(): void {
    this.accessToken = null;
    this.tokenExpiresAt = 0;
  }
}
