import { AXIOS_TIMEOUT_MS, COOKIE_NAME, ONE_YEAR_MS } from "../../shared/const.ts";
import { ForbiddenError } from "../../shared/_core/errors.ts";
import axios, { type AxiosInstance } from "axios";
import { parse as parseCookieHeader } from "cookie";
import type { Request } from "express";
import { SignJWT, jwtVerify } from "jose";
import type { User } from "../../drizzle/schema.ts";
import { getDb, getUserByOpenId, upsertUser } from "../db.ts";
import { ENV } from "./env.ts";
import type {
  ExchangeTokenRequest,
  ExchangeTokenResponse,
  GetUserInfoResponse,
  GetUserInfoWithJwtRequest,
  GetUserInfoWithJwtResponse,
} from "./types/manusTypes.ts";
// Utility function
const isNonEmptyString = (value: unknown): value is string =>
  typeof value === "string" && value.length > 0;

export type SessionPayload = {
  openId: string;
  appId: string;
  name: string;
};

const EXCHANGE_TOKEN_PATH = `/webdev.v1.WebDevAuthPublicService/ExchangeToken`;
const GET_USER_INFO_PATH = `/webdev.v1.WebDevAuthPublicService/GetUserInfo`;
const GET_USER_INFO_WITH_JWT_PATH = `/webdev.v1.WebDevAuthPublicService/GetUserInfoWithJwt`;

class OAuthService {
  private client: ReturnType<typeof axios.create>;
  
  constructor(client: ReturnType<typeof axios.create>) {
    this.client = client;
    console.log("[OAuth] Initialized with baseURL:", ENV.oAuthServerUrl);
    if (!ENV.oAuthServerUrl) {
      console.error(
        "[OAuth] ERROR: OAUTH_SERVER_URL is not configured! Set OAUTH_SERVER_URL environment variable."
      );
    }
  }

  private decodeState(state: string): string {
    try {
      // Convert URL-safe base64 to standard base64
      let standardBase64 = state.replace(/-/g, '+').replace(/_/g, '/');
      // Add padding if needed
      const padding = 4 - (standardBase64.length % 4);
      if (padding !== 4) {
        standardBase64 += '='.repeat(padding);
      }
      // Use Node.js Buffer for base64 decoding (atob not available in Node)
      const decoded = Buffer.from(standardBase64, 'base64').toString('utf8');
      
      // Try to parse as JSON (new format with origin and returnPath)
      try {
        const stateObj = JSON.parse(decoded);
        if (stateObj.origin) {
          // Reconstruct the redirect URI
          return `${stateObj.origin}/api/oauth/callback`;
        }
      } catch {
        // Fall back to treating decoded value as redirectUri string (old format)
      }
      
      return decoded;
    } catch (error) {
      console.error("[SDK] Failed to decode state from base64:", error instanceof Error ? error.message : String(error));
      console.error("[SDK] State value:", state);
      throw new Error("Invalid state format - not valid base64");
    }
  }

  async getTokenByCode(
    code: string,
    state: string,
    redirectUri?: string
  ): Promise<ExchangeTokenResponse> {
    try {
      // Use provided redirectUri or decode from state as fallback
      const finalRedirectUri = redirectUri || this.decodeState(state);
      console.log("[SDK] Using redirectUri:", finalRedirectUri);
      
      const payload: ExchangeTokenRequest = {
        clientId: ENV.appId,
        grantType: "authorization_code",
        code,
        redirectUri: finalRedirectUri,
      };
      
      console.log("[SDK] Exchanging token with clientId:", payload.clientId);

      const { data } = await this.client.post<ExchangeTokenResponse>(
        EXCHANGE_TOKEN_PATH,
        payload
      );
      
      console.log("[SDK] Token exchange response received");
      return data;
    } catch (error) {
      const msg = error instanceof Error ? error.message : String(error);
      console.error("[SDK] Token exchange failed:", msg);
      throw error;
    }
  }

  async getUserInfoByToken(
    token: ExchangeTokenResponse
  ): Promise<GetUserInfoResponse> {
    const { data } = await this.client.post<GetUserInfoResponse>(
      GET_USER_INFO_PATH,
      {
        accessToken: token.accessToken,
      }
    );

    return data;
  }
}

const createOAuthHttpClient = (): AxiosInstance => {
  const client = axios.create({
    baseURL: ENV.oAuthServerUrl,
    timeout: AXIOS_TIMEOUT_MS,
  });
  
  client.interceptors.response.use(
    (response) => response,
    (error) => {
      if (error.response) {
        console.error("[Axios] Response error:", error.response.status, error.response.data);
      } else if (error.request) {
        console.error("[Axios] No response received");
      } else {
        console.error("[Axios] Error:", error.message);
      }
      return Promise.reject(error);
    }
  );
  
  return client;
};

class SDKServer {
  private readonly client: AxiosInstance;
  private readonly oauthService: OAuthService;

  constructor(client: AxiosInstance = createOAuthHttpClient()) {
    this.client = client;
    this.oauthService = new OAuthService(this.client);
  }

  private deriveLoginMethod(
    platforms: unknown,
    fallback: string | null | undefined
  ): string | null {
    if (fallback && fallback.length > 0) return fallback;
    if (!Array.isArray(platforms) || platforms.length === 0) return null;
    const set = new Set<string>(
      platforms.filter((p): p is string => typeof p === "string")
    );
    if (set.has("REGISTERED_PLATFORM_EMAIL")) return "email";
    if (set.has("REGISTERED_PLATFORM_GOOGLE")) return "google";
    if (set.has("REGISTERED_PLATFORM_APPLE")) return "apple";
    if (
      set.has("REGISTERED_PLATFORM_MICROSOFT") ||
      set.has("REGISTERED_PLATFORM_AZURE")
    )
      return "microsoft";
    if (set.has("REGISTERED_PLATFORM_GITHUB")) return "github";
    const first = Array.from(set)[0];
    return first ? first.toLowerCase() : null;
  }

  /**
   * Exchange OAuth authorization code for access token
   * @example
   * const tokenResponse = await sdk.exchangeCodeForToken(code, state);
   */
  async exchangeCodeForToken(
    code: string,
    state: string,
    redirectUri?: string
  ): Promise<ExchangeTokenResponse> {
    return this.oauthService.getTokenByCode(code, state, redirectUri);
  }

  /**
   * Get user information using access token
   * @example
   * const userInfo = await sdk.getUserInfo(tokenResponse.accessToken);
   */
  async getUserInfo(accessToken: string): Promise<GetUserInfoResponse> {
    const data = await this.oauthService.getUserInfoByToken({
      accessToken,
    } as ExchangeTokenResponse);
    const loginMethod = this.deriveLoginMethod(
      (data as any)?.platforms,
      (data as any)?.platform ?? data.platform ?? null
    );
    return {
      ...(data as any),
      platform: loginMethod,
      loginMethod,
    } as GetUserInfoResponse;
  }

  private parseCookies(cookieHeader: string | undefined) {
    if (!cookieHeader) {
      return new Map<string, string>();
    }

    const parsed = parseCookieHeader(cookieHeader);
    return new Map(Object.entries(parsed));
  }

  private getSessionSecret() {
    const secret = ENV.cookieSecret;
    return new TextEncoder().encode(secret);
  }

  /**
   * Create a session token for a Manus user openId
   * @example
   * const sessionToken = await sdk.createSessionToken(userInfo.openId);
   */
  async createSessionToken(
    openId: string,
    options: { expiresInMs?: number; name?: string } = {}
  ): Promise<string> {
    return this.signSession(
      {
        openId,
        appId: ENV.appId,
        name: options.name || "",
      },
      options
    );
  }

  async signSession(
    payload: SessionPayload,
    options: { expiresInMs?: number } = {}
  ): Promise<string> {
    const issuedAt = Date.now();
    const expiresInMs = options.expiresInMs ?? ONE_YEAR_MS;
    const expirationSeconds = Math.floor((issuedAt + expiresInMs) / 1000);
    const secretKey = this.getSessionSecret();

    return new SignJWT({
      openId: payload.openId,
      appId: payload.appId,
      name: payload.name,
    })
      .setProtectedHeader({ alg: "HS256", typ: "JWT" })
      .setExpirationTime(expirationSeconds)
      .sign(secretKey);
  }

  async verifySession(
    cookieValue: string | undefined | null
  ): Promise<{ openId: string; appId: string; name: string } | null> {
    if (!cookieValue) {
      console.warn("[Auth] Missing session cookie");
      return null;
    }

    try {
      const secretKey = this.getSessionSecret();
      const { payload } = await jwtVerify(cookieValue, secretKey, {
        algorithms: ["HS256"],
      });
      const { openId, appId, name } = payload as Record<string, unknown>;

      if (
        !isNonEmptyString(openId) ||
        !isNonEmptyString(appId) ||
        !isNonEmptyString(name)
      ) {
        console.warn("[Auth] Session payload missing required fields");
        return null;
      }

      return {
        openId,
        appId,
        name,
      };
    } catch (error) {
      console.warn("[Auth] Session verification failed", String(error));
      return null;
    }
  }

  async getUserInfoWithJwt(
    jwtToken: string
  ): Promise<GetUserInfoWithJwtResponse> {
    const payload: GetUserInfoWithJwtRequest = {
      jwtToken,
      projectId: ENV.appId,
    };

    const { data } = await this.client.post<GetUserInfoWithJwtResponse>(
      GET_USER_INFO_WITH_JWT_PATH,
      payload
    );

    const loginMethod = this.deriveLoginMethod(
      (data as any)?.platforms,
      (data as any)?.platform ?? data.platform ?? null
    );
    return {
      ...(data as any),
      platform: loginMethod,
      loginMethod,
    } as GetUserInfoWithJwtResponse;
  }

  async authenticateRequest(req: Request): Promise<User> {
    console.log("[Auth] authenticateRequest called");
    console.log("[Auth] Cookies header:", req.headers.cookie);
    // Regular authentication flow
    const cookies = this.parseCookies(req.headers.cookie);
    const sessionCookie = cookies.get(COOKIE_NAME);
    console.log("[Auth] Session cookie found:", !!sessionCookie);
    const session = await this.verifySession(sessionCookie);
    console.log("[Auth] Session verified:", !!session, session);

    if (!session) {
      throw ForbiddenError("Invalid session cookie");
    }

    const sessionUserId = session.openId;
    const signedInAt = new Date();
    console.log("[Auth] Looking up user by openId:", sessionUserId);
    let user = await getUserByOpenId(sessionUserId);
    console.log("[Auth] User found:", !!user, user?.id);

    // If user not in DB, sync from OAuth server automatically
    console.log("[Auth] User not found, attempting to sync from OAuth");
    if (!user) {
      try {
        const userInfo = await this.getUserInfoWithJwt(sessionCookie ?? "");
        await upsertUser({
          openId: userInfo.openId,
          name: userInfo.name || null,
          email: userInfo.email ?? null,
          loginMethod: userInfo.loginMethod ?? userInfo.platform ?? null,
          lastSignedIn: signedInAt,
        });
        user = await getUserByOpenId(userInfo.openId);
      } catch (error) {
        console.error("[Auth] Failed to sync user from OAuth:", error);
        throw ForbiddenError("Failed to sync user info");
      }
    }

    if (!user) {
      throw ForbiddenError("User not found");
    }

    console.log("[Auth] Updating lastSignedIn for user:", user.id);
    await upsertUser({
      openId: user.openId,
      lastSignedIn: signedInAt,
    });

    console.log("[Auth] Authentication successful for user:", user.id);
    return user;
  }
}

export const sdk = new SDKServer();
