import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
import path from "node:path";
import {
  FIXED_DIGITAL_HUMAN_CONFIG,
  FIXED_DOUYIN_RUNTIME_CONFIG,
  FIXED_EXPERT_MODEL_ENDPOINTS,
  type AppConfig,
  type DouyinRuntimeConfig,
  type DouyinTextModelConfig,
  type DigitalHumanModelConfig,
  type ExpertModelConfig,
  type ModelEndpointConfig,
  type RuntimeModePreference,
  type SaveConfigInput,
  type SetupMode,
  type XhsFeishuConfig
} from "@qjclaw/shared-types";

export type RuntimeCloudApiBaseUrlSource = "config" | "env" | "default";

export interface RuntimeCloudApiTarget {
  baseUrl: string;
  source: RuntimeCloudApiBaseUrlSource;
}

const CONFIG_DIR = "config";
const CONFIG_FILE = "app-config.json";
const DEFAULT_RUNTIME_CLOUD_API_BASE_URL = "https://spb-bp1wv2oe0hvfvi98.supabase.opentrust.net/functions/v1";
const DEPRECATED_RUNTIME_CLOUD_API_BASE_URLS = new Set([
  "https://xuphfkscoptnjoaecbvn.supabase.co/functions/v1"
]);
const UI_ROUTE_NAMES = new Set([
  "chat",
  "control",
  "settings",
  "logs",
  "config",
  "agent",
  "cron",
  "skills",
  "nodes",
  "usage",
  "sessions",
  "overview",
  "instances",
  "channels"
]);

interface LegacyConfig {
  setupMode?: SetupMode;
  provider?: string;
  baseUrl?: string;
  apiKeyConfigured?: boolean;
  defaultModel?: string;
  workspacePath?: string;
  gatewayPort?: number;
  gatewayUrl?: string;
  gatewayTokenConfigured?: boolean;
  authTokenConfigured?: boolean;
  cloudApiBaseUrl?: string;
  runtimeCloudApiBaseUrl?: string;
  runtimeMode?: RuntimeModePreference;
  expertModelConfig?: {
    image?: Partial<ModelEndpointConfig>;
    video?: Partial<ModelEndpointConfig>;
    copywriting?: Partial<ModelEndpointConfig>;
    digitalHuman?: Partial<DigitalHumanModelConfig>;
  };
  douyinRuntimeConfig?: {
    videoAnalyzer?: Partial<DouyinTextModelConfig>;
    replicationBrief?: Partial<DouyinTextModelConfig>;
    vectcut?: {
      baseUrl?: string;
      fileBaseUrl?: string;
      apiKeyConfigured?: boolean;
    };
  };
  xhsFeishuConfig?: Partial<XhsFeishuConfig>;
}

function normalizeGatewayUrl(raw: string): string {
  const trimmed = raw.trim();
  if (!trimmed) {
    return "ws://127.0.0.1:18789";
  }

  try {
    const url = new URL(trimmed);
    if (url.protocol === "ws:" || url.protocol === "wss:") {
      url.hash = "";
      url.search = "";
      return url.toString().replace(/\/$/, "");
    }

    if (url.protocol === "http:" || url.protocol === "https:") {
      const route = url.pathname.split("/").filter(Boolean)[0] ?? "";
      const basePath = UI_ROUTE_NAMES.has(route)
        ? "/"
        : url.pathname.endsWith("index.html")
          ? url.pathname.slice(0, -"index.html".length)
          : url.pathname;
      url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
      url.pathname = basePath || "/";
      url.search = "";
      url.hash = "";
      return url.toString().replace(/\/$/, "");
    }
  } catch {
    return trimmed;
  }

  return trimmed;
}

function normalizeCloudApiBaseUrl(raw: string): string {
  return raw.trim().replace(/\/$/, "");
}

function normalizeRuntimeMode(raw?: string): RuntimeModePreference {
  return raw === "bundled-runtime" || raw === "external-gateway" ? raw : "bundled-runtime";
}

function normalizeSetupMode(raw?: string): SetupMode {
  return raw === "direct-provider" ? raw : "employee-key";
}

function normalizeRuntimeCloudApiBaseUrl(raw?: string): string {
  return normalizeCloudApiBaseUrl(raw ?? "");
}

function resolveRuntimeCloudApiTarget(raw?: string): RuntimeCloudApiTarget {
  const configValue = migrateDeprecatedRuntimeCloudApiBaseUrl(raw);
  if (configValue) {
    return {
      baseUrl: configValue,
      source: "config"
    };
  }

  const envValue = migrateDeprecatedRuntimeCloudApiBaseUrl(process.env.QJCLAW_RUNTIME_CLOUD_API_BASE_URL);
  if (envValue) {
    return {
      baseUrl: envValue,
      source: "env"
    };
  }

  return {
    baseUrl: DEFAULT_RUNTIME_CLOUD_API_BASE_URL,
    source: "default"
  };
}

function migrateDeprecatedRuntimeCloudApiBaseUrl(raw?: string): string {
  const normalized = normalizeRuntimeCloudApiBaseUrl(raw);
  if (normalized && DEPRECATED_RUNTIME_CLOUD_API_BASE_URLS.has(normalized)) {
    return DEFAULT_RUNTIME_CLOUD_API_BASE_URL;
  }
  return normalized;
}

function createFixedModelEndpointConfig(kind: keyof typeof FIXED_EXPERT_MODEL_ENDPOINTS): ModelEndpointConfig {
  const config = FIXED_EXPERT_MODEL_ENDPOINTS[kind];
  return {
    baseUrl: config.baseUrl,
    apiKeyConfigured: false,
    modelId: config.modelId
  };
}

function createDefaultDigitalHumanModelConfig(): DigitalHumanModelConfig {
  return {
    ...FIXED_DIGITAL_HUMAN_CONFIG,
    volcAccessKeyConfigured: false,
    volcSecretKeyConfigured: false,
    qiniuAccessKeyConfigured: false,
    qiniuSecretKeyConfigured: false
  };
}

function createDefaultExpertModelConfig(): ExpertModelConfig {
  return {
    image: createFixedModelEndpointConfig("image"),
    video: createFixedModelEndpointConfig("video"),
    copywriting: createFixedModelEndpointConfig("copywriting"),
    digitalHuman: createDefaultDigitalHumanModelConfig()
  };
}

function createDefaultDouyinTextModelConfig(kind: "videoAnalyzer" | "replicationBrief"): DouyinTextModelConfig {
  const config = FIXED_DOUYIN_RUNTIME_CONFIG[kind];
  return {
    baseUrl: config.baseUrl,
    apiKeyConfigured: false,
    modelId: config.modelId
  };
}

function createDefaultDouyinRuntimeConfig(): DouyinRuntimeConfig {
  return {
    videoAnalyzer: createDefaultDouyinTextModelConfig("videoAnalyzer"),
    replicationBrief: createDefaultDouyinTextModelConfig("replicationBrief"),
    vectcut: {
      baseUrl: FIXED_DOUYIN_RUNTIME_CONFIG.vectcut.baseUrl,
      fileBaseUrl: FIXED_DOUYIN_RUNTIME_CONFIG.vectcut.fileBaseUrl,
      apiKeyConfigured: false
    }
  };
}

function createDefaultXhsFeishuConfig(): XhsFeishuConfig {
  return {
    appIdConfigured: false,
    appSecretConfigured: false,
    appTokenConfigured: false,
    tableIdConfigured: false
  };
}

function mergeExpertModelConfig(
  current: ExpertModelConfig,
  input?: SaveConfigInput["expertModelConfig"]
): ExpertModelConfig {
  return {
    image: {
      baseUrl: FIXED_EXPERT_MODEL_ENDPOINTS.image.baseUrl,
      apiKeyConfigured: typeof input?.image?.apiKey === "string" ? Boolean(input.image.apiKey.trim()) : current.image.apiKeyConfigured,
      modelId: FIXED_EXPERT_MODEL_ENDPOINTS.image.modelId
    },
    video: {
      baseUrl: FIXED_EXPERT_MODEL_ENDPOINTS.video.baseUrl,
      apiKeyConfigured: typeof input?.video?.apiKey === "string" ? Boolean(input.video.apiKey.trim()) : current.video.apiKeyConfigured,
      modelId: FIXED_EXPERT_MODEL_ENDPOINTS.video.modelId
    },
    copywriting: {
      baseUrl: FIXED_EXPERT_MODEL_ENDPOINTS.copywriting.baseUrl,
      apiKeyConfigured: typeof input?.copywriting?.apiKey === "string" ? Boolean(input.copywriting.apiKey.trim()) : current.copywriting.apiKeyConfigured,
      modelId: FIXED_EXPERT_MODEL_ENDPOINTS.copywriting.modelId
    },
    digitalHuman: {
      ...FIXED_DIGITAL_HUMAN_CONFIG,
      volcAccessKeyConfigured: typeof input?.digitalHuman?.volcAccessKey === "string" ? Boolean(input.digitalHuman.volcAccessKey.trim()) : current.digitalHuman.volcAccessKeyConfigured,
      volcSecretKeyConfigured: typeof input?.digitalHuman?.volcSecretKey === "string" ? Boolean(input.digitalHuman.volcSecretKey.trim()) : current.digitalHuman.volcSecretKeyConfigured,
      qiniuAccessKeyConfigured: typeof input?.digitalHuman?.qiniuAccessKey === "string" ? Boolean(input.digitalHuman.qiniuAccessKey.trim()) : current.digitalHuman.qiniuAccessKeyConfigured,
      qiniuSecretKeyConfigured: typeof input?.digitalHuman?.qiniuSecretKey === "string" ? Boolean(input.digitalHuman.qiniuSecretKey.trim()) : current.digitalHuman.qiniuSecretKeyConfigured
    }
  };
}

function mergeDouyinRuntimeConfig(
  current: DouyinRuntimeConfig,
  input?: SaveConfigInput["douyinRuntimeConfig"]
): DouyinRuntimeConfig {
  return {
    videoAnalyzer: {
      baseUrl: typeof input?.videoAnalyzer?.baseUrl === "string"
        ? input.videoAnalyzer.baseUrl.trim()
        : current.videoAnalyzer.baseUrl,
      apiKeyConfigured: typeof input?.videoAnalyzer?.apiKey === "string"
        ? Boolean(input.videoAnalyzer.apiKey.trim())
        : current.videoAnalyzer.apiKeyConfigured,
      modelId: typeof input?.videoAnalyzer?.modelId === "string"
        ? input.videoAnalyzer.modelId.trim()
        : current.videoAnalyzer.modelId
    },
    replicationBrief: {
      baseUrl: typeof input?.replicationBrief?.baseUrl === "string"
        ? input.replicationBrief.baseUrl.trim()
        : current.replicationBrief.baseUrl,
      apiKeyConfigured: typeof input?.replicationBrief?.apiKey === "string"
        ? Boolean(input.replicationBrief.apiKey.trim())
        : current.replicationBrief.apiKeyConfigured,
      modelId: typeof input?.replicationBrief?.modelId === "string"
        ? input.replicationBrief.modelId.trim()
        : current.replicationBrief.modelId
    },
    vectcut: {
      baseUrl: typeof input?.vectcut?.baseUrl === "string"
        ? input.vectcut.baseUrl.trim()
        : current.vectcut.baseUrl,
      fileBaseUrl: typeof input?.vectcut?.fileBaseUrl === "string"
        ? input.vectcut.fileBaseUrl.trim()
        : current.vectcut.fileBaseUrl,
      apiKeyConfigured: typeof input?.vectcut?.apiKey === "string"
        ? Boolean(input.vectcut.apiKey.trim())
        : current.vectcut.apiKeyConfigured
    }
  };
}

function mergeXhsFeishuConfig(
  current: XhsFeishuConfig,
  input?: SaveConfigInput["xhsFeishuConfig"]
): XhsFeishuConfig {
  return {
    appIdConfigured: typeof input?.appId === "string" ? Boolean(input.appId.trim()) : current.appIdConfigured,
    appSecretConfigured: typeof input?.appSecret === "string" ? Boolean(input.appSecret.trim()) : current.appSecretConfigured,
    appTokenConfigured: typeof input?.appToken === "string" ? Boolean(input.appToken.trim()) : current.appTokenConfigured,
    tableIdConfigured: typeof input?.tableId === "string" ? Boolean(input.tableId.trim()) : current.tableIdConfigured
  };
}

export function getRuntimeCloudApiTarget(config: Pick<AppConfig, "runtimeCloudApiBaseUrl">): RuntimeCloudApiTarget {
  return resolveRuntimeCloudApiTarget(config.runtimeCloudApiBaseUrl);
}

export class AppConfigService {
  private readonly userDataPath: string;
  private ioChain: Promise<void> = Promise.resolve();

  constructor(userDataPath: string) {
    this.userDataPath = userDataPath;
  }

  async load(): Promise<AppConfig> {
    return this.runExclusive(() => this.loadUnlocked());
  }

  async save(input: SaveConfigInput): Promise<AppConfig> {
    return this.runExclusive(async () => {
      const current = await this.loadUnlocked();
      const config: AppConfig = {
        setupMode: normalizeSetupMode(input.setupMode),
        provider: input.provider,
        baseUrl: input.baseUrl,
        apiKeyConfigured: Boolean(input.apiKey) || current.apiKeyConfigured,
        gatewayTokenConfigured: Boolean(input.gatewayToken) || current.gatewayTokenConfigured,
        authTokenConfigured: typeof input.authToken === "string" ? Boolean(input.authToken) : current.authTokenConfigured,
        defaultModel: input.defaultModel,
        workspacePath: input.workspacePath,
        gatewayUrl: normalizeGatewayUrl(input.gatewayUrl),
        cloudApiBaseUrl: normalizeCloudApiBaseUrl(input.cloudApiBaseUrl),
        runtimeCloudApiBaseUrl: migrateDeprecatedRuntimeCloudApiBaseUrl(input.runtimeCloudApiBaseUrl),
        runtimeMode: normalizeRuntimeMode(input.runtimeMode),
        expertModelConfig: mergeExpertModelConfig(current.expertModelConfig, input.expertModelConfig),
        douyinRuntimeConfig: mergeDouyinRuntimeConfig(current.douyinRuntimeConfig, input.douyinRuntimeConfig),
        xhsFeishuConfig: mergeXhsFeishuConfig(current.xhsFeishuConfig, input.xhsFeishuConfig)
      };

      await this.writeConfig(config);
      return config;
    });
  }

  async persist(config: AppConfig): Promise<void> {
    return this.runExclusive(async () => {
      await this.writeConfig(config);
    });
  }

  getDataPath(...segments: string[]): string {
    return path.join(this.userDataPath, ...segments);
  }

  private getConfigPath(): string {
    return path.join(this.userDataPath, CONFIG_DIR, CONFIG_FILE);
  }

  private createDefaultConfig(): AppConfig {
    return {
      setupMode: "employee-key",
      provider: "openai",
      baseUrl: "https://api.openai.com/v1",
      apiKeyConfigured: false,
      gatewayTokenConfigured: false,
      authTokenConfigured: false,
      defaultModel: "gpt-5.4-mini",
      workspacePath: this.userDataPath,
      gatewayUrl: "ws://127.0.0.1:18789",
      cloudApiBaseUrl: normalizeCloudApiBaseUrl(process.env.QJCLAW_CLOUD_API_BASE_URL ?? ""),
      runtimeCloudApiBaseUrl: "",
      runtimeMode: normalizeRuntimeMode(process.env.QJCLAW_RUNTIME_MODE),
      expertModelConfig: createDefaultExpertModelConfig(),
      douyinRuntimeConfig: createDefaultDouyinRuntimeConfig(),
      xhsFeishuConfig: createDefaultXhsFeishuConfig()
    };
  }

  private normalizeConfig(config: LegacyConfig): AppConfig {
    const defaultExpertModelConfig = createDefaultExpertModelConfig();
    const defaultDouyinRuntimeConfig = createDefaultDouyinRuntimeConfig();
    const defaultXhsFeishuConfig = createDefaultXhsFeishuConfig();
    return {
      setupMode: normalizeSetupMode(config.setupMode),
      provider: config.provider ?? "openai",
      baseUrl: config.baseUrl ?? "https://api.openai.com/v1",
      apiKeyConfigured: Boolean(config.apiKeyConfigured),
      gatewayTokenConfigured: Boolean(config.gatewayTokenConfigured),
      authTokenConfigured: Boolean(config.authTokenConfigured),
      defaultModel: config.defaultModel ?? "gpt-5.4-mini",
      workspacePath: config.workspacePath ?? this.userDataPath,
      gatewayUrl: normalizeGatewayUrl(config.gatewayUrl ?? `ws://127.0.0.1:${config.gatewayPort ?? 18789}`),
      cloudApiBaseUrl: normalizeCloudApiBaseUrl(config.cloudApiBaseUrl ?? process.env.QJCLAW_CLOUD_API_BASE_URL ?? ""),
      runtimeCloudApiBaseUrl: migrateDeprecatedRuntimeCloudApiBaseUrl(config.runtimeCloudApiBaseUrl),
      runtimeMode: normalizeRuntimeMode(config.runtimeMode ?? process.env.QJCLAW_RUNTIME_MODE),
      expertModelConfig: {
        image: {
          baseUrl: defaultExpertModelConfig.image.baseUrl,
          apiKeyConfigured: Boolean(config.expertModelConfig?.image?.apiKeyConfigured),
          modelId: defaultExpertModelConfig.image.modelId
        },
        video: {
          baseUrl: defaultExpertModelConfig.video.baseUrl,
          apiKeyConfigured: Boolean(config.expertModelConfig?.video?.apiKeyConfigured),
          modelId: defaultExpertModelConfig.video.modelId
        },
        copywriting: {
          baseUrl: defaultExpertModelConfig.copywriting.baseUrl,
          apiKeyConfigured: Boolean(config.expertModelConfig?.copywriting?.apiKeyConfigured),
          modelId: defaultExpertModelConfig.copywriting.modelId
        },
        digitalHuman: {
          ...FIXED_DIGITAL_HUMAN_CONFIG,
          volcAccessKeyConfigured: Boolean(config.expertModelConfig?.digitalHuman?.volcAccessKeyConfigured),
          volcSecretKeyConfigured: Boolean(config.expertModelConfig?.digitalHuman?.volcSecretKeyConfigured),
          qiniuAccessKeyConfigured: Boolean(config.expertModelConfig?.digitalHuman?.qiniuAccessKeyConfigured),
          qiniuSecretKeyConfigured: Boolean(config.expertModelConfig?.digitalHuman?.qiniuSecretKeyConfigured)
        }
      },
      douyinRuntimeConfig: {
        videoAnalyzer: {
          baseUrl: typeof config.douyinRuntimeConfig?.videoAnalyzer?.baseUrl === "string"
            ? config.douyinRuntimeConfig.videoAnalyzer.baseUrl
            : defaultDouyinRuntimeConfig.videoAnalyzer.baseUrl,
          apiKeyConfigured: Boolean(config.douyinRuntimeConfig?.videoAnalyzer?.apiKeyConfigured),
          modelId: config.douyinRuntimeConfig?.videoAnalyzer?.modelId ?? defaultDouyinRuntimeConfig.videoAnalyzer.modelId
        },
        replicationBrief: {
          baseUrl: typeof config.douyinRuntimeConfig?.replicationBrief?.baseUrl === "string"
            ? config.douyinRuntimeConfig.replicationBrief.baseUrl
            : defaultDouyinRuntimeConfig.replicationBrief.baseUrl,
          apiKeyConfigured: Boolean(config.douyinRuntimeConfig?.replicationBrief?.apiKeyConfigured),
          modelId: config.douyinRuntimeConfig?.replicationBrief?.modelId ?? defaultDouyinRuntimeConfig.replicationBrief.modelId
        },
        vectcut: {
          baseUrl: typeof config.douyinRuntimeConfig?.vectcut?.baseUrl === "string"
            ? config.douyinRuntimeConfig.vectcut.baseUrl
            : defaultDouyinRuntimeConfig.vectcut.baseUrl,
          fileBaseUrl: typeof config.douyinRuntimeConfig?.vectcut?.fileBaseUrl === "string"
            ? config.douyinRuntimeConfig.vectcut.fileBaseUrl
            : defaultDouyinRuntimeConfig.vectcut.fileBaseUrl,
          apiKeyConfigured: Boolean(config.douyinRuntimeConfig?.vectcut?.apiKeyConfigured)
        }
      },
      xhsFeishuConfig: {
        appIdConfigured: Boolean(config.xhsFeishuConfig?.appIdConfigured ?? defaultXhsFeishuConfig.appIdConfigured),
        appSecretConfigured: Boolean(config.xhsFeishuConfig?.appSecretConfigured ?? defaultXhsFeishuConfig.appSecretConfigured),
        appTokenConfigured: Boolean(config.xhsFeishuConfig?.appTokenConfigured ?? defaultXhsFeishuConfig.appTokenConfigured),
        tableIdConfigured: Boolean(config.xhsFeishuConfig?.tableIdConfigured ?? defaultXhsFeishuConfig.tableIdConfigured)
      }
    };
  }

  private async runExclusive<T>(operation: () => Promise<T>): Promise<T> {
    const next = this.ioChain.then(operation, operation);
    this.ioChain = next.then(() => undefined, () => undefined);
    return next;
  }

  private async loadUnlocked(): Promise<AppConfig> {
    const filePath = this.getConfigPath();
    await mkdir(path.dirname(filePath), { recursive: true });

    try {
      const raw = await readFile(filePath, "utf8");
      const parsed = JSON.parse(raw) as LegacyConfig;
      const config = this.normalizeConfig(parsed);
      await this.writeConfig(config);
      return config;
    } catch (error) {
      if (!this.shouldResetToDefaults(error)) {
        throw error;
      }

      const defaults = this.createDefaultConfig();
      await this.writeConfig(defaults);
      return defaults;
    }
  }

  private shouldResetToDefaults(error: unknown): boolean {
    if (!error || typeof error !== "object") {
      return false;
    }

    const candidate = error as NodeJS.ErrnoException;
    if (candidate.code === "ENOENT") {
      return true;
    }

    return error instanceof SyntaxError;
  }

  private async writeConfig(config: AppConfig): Promise<void> {
    const filePath = this.getConfigPath();
    const tempPath = `${filePath}.tmp`;
    await mkdir(path.dirname(filePath), { recursive: true });
    await writeFile(tempPath, JSON.stringify(config, null, 2), "utf8");
    await rm(filePath, { force: true });
    await rename(tempPath, filePath);
  }
}
