#!/usr/bin/env bun

const SCHEMA_VERSION = "tjwater-cli/v1";
const DEFAULT_SERVER = "http://192.168.1.114:8000";
const DEFAULT_TIMEOUT = 180;

const GROUPS = {
  network: "管网节点、管线等基础属性查询命令。",
  component: "组件选项与配置读取命令。",
  simulation: "模拟运行与调度相关命令。",
  analysis: "分析计算与诊断相关命令。",
  data: "时序、SCADA 和方案数据查询命令。",
};

const NETWORK_COMMANDS = {
  "get-junction-properties": {
    summary: "读取节点属性成功",
    path: "/getjunctionproperties/",
    options: { junction: { required: true } },
    params: ({ args, ctx }) => ({ network: requireNetwork(ctx), junction: args.junction }),
  },
  "get-pipe-properties": {
    summary: "读取管道属性成功",
    path: "/getpipeproperties/",
    options: { pipe: { required: true } },
    params: ({ args, ctx }) => ({ network: requireNetwork(ctx), pipe: args.pipe }),
  },
  "get-all-pipes-properties": {
    summary: "读取全部管道属性成功",
    path: "/getallpipeproperties/",
    params: ({ ctx }) => ({ network: requireNetwork(ctx) }),
  },
  "get-reservoir-properties": {
    summary: "读取水库属性成功",
    path: "/getreservoirproperties/",
    options: { reservoir: { required: true } },
    params: ({ args, ctx }) => ({ network: requireNetwork(ctx), reservoir: args.reservoir }),
  },
  "get-all-reservoirs-properties": {
    summary: "读取全部水库属性成功",
    path: "/getallreservoirproperties/",
    params: ({ ctx }) => ({ network: requireNetwork(ctx) }),
  },
  "get-tank-properties": {
    summary: "读取水箱属性成功",
    path: "/gettankproperties/",
    options: { tank: { required: true } },
    params: ({ args, ctx }) => ({ network: requireNetwork(ctx), tank: args.tank }),
  },
  "get-all-tanks-properties": {
    summary: "读取全部水箱属性成功",
    path: "/getalltankproperties/",
    params: ({ ctx }) => ({ network: requireNetwork(ctx) }),
  },
  "get-pump-properties": {
    summary: "读取水泵属性成功",
    path: "/getpumpproperties/",
    options: { pump: { required: true } },
    params: ({ args, ctx }) => ({ network: requireNetwork(ctx), pump: args.pump }),
  },
  "get-all-pumps-properties": {
    summary: "读取全部水泵属性成功",
    path: "/getallpumpproperties/",
    params: ({ ctx }) => ({ network: requireNetwork(ctx) }),
  },
  "get-valve-properties": {
    summary: "读取阀门属性成功",
    path: "/getvalveproperties/",
    options: { valve: { required: true } },
    params: ({ args, ctx }) => ({ network: requireNetwork(ctx), valve: args.valve }),
  },
  "get-all-valves-properties": {
    summary: "读取全部阀门属性成功",
    path: "/getallvalveproperties/",
    params: ({ ctx }) => ({ network: requireNetwork(ctx) }),
  },
};

const DATA_REALTIME_COMMANDS = {
  links: {
    summary: "读取实时管道数据成功",
    path: "/realtime/links",
    options: { "start-time": { required: true }, "end-time": { required: true } },
    requireProject: true,
    params: ({ args }) => ({
      start_time: parseTime(args["start-time"], "--start-time"),
      end_time: parseTime(args["end-time"], "--end-time"),
    }),
  },
  nodes: {
    summary: "读取实时节点数据成功",
    path: "/realtime/nodes",
    options: { "start-time": { required: true }, "end-time": { required: true } },
    requireProject: true,
    params: ({ args }) => ({
      start_time: parseTime(args["start-time"], "--start-time"),
      end_time: parseTime(args["end-time"], "--end-time"),
    }),
  },
  "simulation-by-id-time": {
    summary: "读取实时模拟数据成功",
    path: "/realtime/query/by-id-time",
    options: { id: { required: true }, type: { required: true }, time: { required: true } },
    requireProject: true,
    params: ({ args }) => ({
      id: args.id,
      type: args.type,
      query_time: parseTime(args.time, "--time"),
    }),
  },
  "simulation-by-time-property": {
    summary: "读取实时属性聚合数据成功",
    path: "/realtime/query/by-time-property",
    options: { type: { required: true }, time: { required: true }, property: { required: true } },
    requireProject: true,
    params: ({ args }) => ({
      type: args.type,
      query_time: parseTime(args.time, "--time"),
      property: args.property,
    }),
  },
};

const DATA_SCHEME_COMMANDS = {
  list: {
    summary: "读取方案列表成功",
    path: "/schemes",
    requireProject: true,
    params: ({ args }) => maybe({ scheme_type: args["scheme-type"] }),
  },
  get: {
    summary: "读取方案详情成功",
    path: ({ positionals }) => `/schemes/${encodeURIComponent(requirePos(positionals, 0, "scheme name"))}`,
    requireProject: true,
    params: ({ args }) => maybe({ scheme_type: args["scheme-type"] }),
  },
  links: {
    summary: "读取方案管道数据成功",
    path: "/scheme/links",
    options: { "start-time": { required: true }, "end-time": { required: true } },
    requireProject: true,
    params: ({ args, ctx }) => ({
      scheme_name: resolveScheme(ctx, args.scheme, true),
      scheme_type: args["scheme-type"] ?? "simulation",
      start_time: parseTime(args["start-time"], "--start-time"),
      end_time: parseTime(args["end-time"], "--end-time"),
    }),
  },
};

const DATA_SCADA_COMMANDS = {
  list: {
    summary: "读取 SCADA 元数据成功",
    path: "/scada/devices",
    requireProject: true,
    params: ({ args }) => maybe({ kind: args.kind }),
  },
  query: {
    summary: "读取 SCADA 时序成功",
    path: ({ args }) => (args.field ? "/scada/by-ids-field-time-range" : "/scada/by-ids-time-range"),
    options: { "device-id": { required: true, repeated: true }, "start-time": { required: true }, "end-time": { required: true } },
    requireProject: true,
    params: ({ args }) => ({
      device_ids: toArray(args["device-id"]).join(","),
      start_time: parseTime(args["start-time"], "--start-time"),
      end_time: parseTime(args["end-time"], "--end-time"),
      ...maybe({ field: args.field }),
    }),
  },
};

async function main() {
  const parsed = parseArgs(Bun.argv.slice(2));
  if (parsed.help || parsed.command.length === 0) {
    emitHelp(parsed.command);
    return 0;
  }
  if (parsed.command[0] === "help") {
    emitHelp(parsed.command.slice(1));
    return 0;
  }

  const ctx = await buildContext(parsed.global);
  const startedAt = Date.now();
  const result = await dispatch(parsed, ctx);
  const data = await requestJson(ctx, result);
  emitSuccess({
    summary: result.summary,
    data,
    ctx,
    durationMs: Date.now() - startedAt,
    nextCommands: result.nextCommands,
  });
  return 0;
}

async function dispatch(parsed, ctx) {
  const [group, ...rest] = parsed.command;
  if (group === "network") {
    return commandFromMap(NETWORK_COMMANDS, rest, parsed, ctx);
  }
  if (group === "component" && rest[0] === "option") {
    return componentOption(rest.slice(1), parsed, ctx);
  }
  if (group === "simulation" && rest[0] === "run") {
    requireOptions(parsed.args, { "start-time": { required: true }, duration: { required: true } });
    const start = parseTime(parsed.args["start-time"], "--start-time");
    const duration = Number(parsed.args.duration);
    const endTime = new Date(Date.parse(start) + duration * 60_000).toISOString();
    return {
      summary: "触发模拟成功",
      method: "POST",
      path: "/runsimulationmanuallybydate/",
      body: { name: requireNetwork(ctx), start_time: start, duration },
      nextCommands: [
        `tjwater-cli data timeseries realtime links --start-time ${start} --end-time ${endTime}`,
        `tjwater-cli data timeseries realtime nodes --start-time ${start} --end-time ${endTime}`,
      ],
    };
  }
  if (group === "analysis") {
    return analysis(rest, parsed, ctx);
  }
  if (group === "data") {
    return dataCommand(rest, parsed, ctx);
  }
  throw cliError("未找到命令", "COMMAND_NOT_FOUND", `unknown command: ${parsed.command.join(" ")}`, 2, {
    next_commands: ["tjwater-cli help"],
  });
}

function commandFromMap(map, commandPath, parsed, ctx) {
  const name = commandPath[0];
  const spec = map[name];
  if (!spec) {
    throw cliError("未找到命令", "COMMAND_NOT_FOUND", `unknown command: ${parsed.command.join(" ")}`, 2, {
      next_commands: ["tjwater-cli help"],
    });
  }
  requireOptions(parsed.args, spec.options ?? {});
  const positionals = [...commandPath.slice(1), ...parsed.positionals];
  return {
    summary: spec.summary,
    method: spec.method ?? "GET",
    path: valueOf(spec.path, { args: parsed.args, positionals, ctx }),
    params: spec.params?.({ args: parsed.args, positionals, ctx }) ?? {},
    body: spec.body?.({ args: parsed.args, positionals, ctx }),
    requireProject: spec.requireProject,
    nextCommands: spec.nextCommands,
  };
}

function componentOption(rest, parsed, ctx) {
  const action = rest[0];
  if (action !== "schema" && action !== "get") {
    throw cliError("未找到命令", "COMMAND_NOT_FOUND", "unknown component option command", 2);
  }
  requireOptions(parsed.args, { kind: { required: true } });
  const kind = parsed.args.kind;
  const routes = {
    "time:schema": "/gettimeschema",
    "time:get": "/gettimeproperties/",
    "energy:schema": "/getenergyschema/",
    "energy:get": "/getenergyproperties/",
    "pump-energy:schema": "/getpumpenergyschema/",
    "pump-energy:get": "/getpumpenergyproperties//",
    "network:schema": "/getoptionschema/",
    "network:get": "/getoptionproperties/",
  };
  const path = routes[`${kind}:${action}`];
  if (!path) {
    throw cliError("CLI 参数错误", "INVALID_KIND", "--kind must be one of time, energy, pump-energy, network", 2);
  }
  if (kind === "pump-energy" && action === "get" && !parsed.args.pump) {
    throw cliError("CLI 参数错误", "PUMP_REQUIRED", "--pump is required when --kind pump-energy", 2);
  }
  return {
    summary: action === "schema" ? "读取选项 schema 成功" : "读取选项属性成功",
    method: "GET",
    path,
    params: { network: requireNetwork(ctx), ...maybe({ pump: parsed.args.pump }) },
  };
}

function analysis(rest, parsed, ctx) {
  const [domain, sub, leaf] = rest;
  if (domain === "age") {
    requireOptions(parsed.args, { "start-time": { required: true }, duration: { required: true } });
    return {
      summary: "水龄分析执行成功",
      method: "GET",
      path: "/age_analysis/",
      params: {
        network: requireNetwork(ctx),
        start_time: parseTime(parsed.args["start-time"], "--start-time"),
        duration: Number(parsed.args.duration),
      },
    };
  }
  if (domain === "leakage" && sub === "schemes") {
    return schemesCommand("漏损", "/leakage/schemes", leaf, rest[3], parsed, ctx);
  }
  if (domain === "burst-detection" && sub === "schemes") {
    return schemesCommand("爆管检测", "/burst-detection/schemes", leaf, rest[3], parsed, ctx);
  }
  if (domain === "leakage" && sub === "identify") {
    return detectionCommand("漏损识别执行成功", "/leakage/identify/", parsed, ctx);
  }
  if (domain === "burst-detection" && sub === "detect") {
    return detectionCommand("爆管检测执行成功", "/burst-detection/detect/", parsed, ctx);
  }
  if (domain === "sensor-placement" && sub === "kmeans") {
    requireOptions(parsed.args, { count: { required: true } });
    return {
      summary: "传感器选址执行成功",
      method: "POST",
      path: "/pressure_sensor_placement_kmeans/",
      body: {
        name: requireNetwork(ctx),
        scheme_name: resolveScheme(ctx, parsed.args.scheme, true),
        sensor_number: Number(parsed.args.count),
        min_diameter: Number(parsed.args["min-diameter"] ?? 0),
        username: requireUsername(ctx),
      },
    };
  }
  throw cliError("未找到命令", "COMMAND_NOT_FOUND", `unknown analysis command: ${rest.join(" ")}`, 2);
}

function schemesCommand(label, basePath, action, inlineName, parsed, ctx) {
  if (action === "list") {
    return {
      summary: `读取${label}方案列表成功`,
      method: "GET",
      path: `${basePath}/`,
      params: { network: requireNetwork(ctx) },
    };
  }
  if (action === "get") {
    const name = inlineName ?? requirePos(parsed.positionals, 0, "scheme name");
    return {
      summary: `读取${label}方案详情成功`,
      method: "GET",
      path: `${basePath}/${encodeURIComponent(name)}`,
      params: { network: requireNetwork(ctx) },
    };
  }
  throw cliError("未找到命令", "COMMAND_NOT_FOUND", "unknown schemes command", 2);
}

function detectionCommand(summary, path, parsed, ctx) {
  requireOptions(parsed.args, { "start-time": { required: true }, "end-time": { required: true } });
  return {
    summary,
    method: "POST",
    path,
    body: {
      network: requireNetwork(ctx),
      scada_start: parseTime(parsed.args["start-time"], "--start-time"),
      scada_end: parseTime(parsed.args["end-time"], "--end-time"),
      scheme_name: resolveScheme(ctx, parsed.args.scheme, true),
    },
  };
}

function dataCommand(rest, parsed, ctx) {
  if (rest[0] === "timeseries" && rest[1] === "realtime") {
    return commandFromMap(DATA_REALTIME_COMMANDS, rest.slice(2), parsed, ctx);
  }
  if (rest[0] === "timeseries" && rest[1] === "scheme") {
    return commandFromMap(DATA_SCHEME_COMMANDS, rest.slice(2), parsed, ctx);
  }
  if (rest[0] === "timeseries" && rest[1] === "scada") {
    return commandFromMap(DATA_SCADA_COMMANDS, rest.slice(2), parsed, ctx);
  }
  if (rest[0] === "scada") {
    return commandFromMap(DATA_SCADA_COMMANDS, rest.slice(1), parsed, ctx);
  }
  if (rest[0] === "scheme") {
    return commandFromMap(DATA_SCHEME_COMMANDS, rest.slice(1), parsed, ctx);
  }
  throw cliError("未找到命令", "COMMAND_NOT_FOUND", `unknown data command: ${rest.join(" ")}`, 2);
}

async function requestJson(ctx, request) {
  if (request.requireProject) {
    requireProject(ctx);
  }
  const url = new URL(valueOf(request.path, { args: {}, positionals: [], ctx }), ctx.server);
  for (const [key, value] of Object.entries(request.params ?? {})) {
    if (value === undefined || value === null || value === "") continue;
    if (Array.isArray(value)) {
      for (const item of value) url.searchParams.append(key, String(item));
      continue;
    }
    url.searchParams.set(key, String(value));
  }

  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), ctx.timeout * 1000);
  const headers = {
    Accept: "application/json",
    "X-Request-Id": ctx.requestId,
    ...ctx.auth.headers,
  };
  if (ctx.auth.accessToken) headers.Authorization = `Bearer ${ctx.auth.accessToken}`;
  if (ctx.auth.projectId) headers["X-Project-Id"] = ctx.auth.projectId;
  if (ctx.auth.userId) headers["X-User-Id"] = ctx.auth.userId;
  if (request.body !== undefined) headers["Content-Type"] = "application/json";

  try {
    const response = await fetch(url, {
      method: request.method ?? "GET",
      headers,
      body: request.body === undefined ? undefined : JSON.stringify(request.body),
      signal: controller.signal,
    });
    const text = await response.text();
    let data = text;
    try {
      data = text ? JSON.parse(text) : null;
    } catch {}
    if (!response.ok) {
      throw cliError("服务端请求失败", "HTTP_ERROR", `server returned HTTP ${response.status}`, response.status >= 500 ? 4 : 2, {
        retryable: response.status >= 500,
        data: { status: response.status, body: data },
      });
    }
    return data;
  } catch (error) {
    if (error?.name === "AbortError") {
      throw cliError("命令超时", "TIMEOUT", `request timed out after ${ctx.timeout}s`, 4, { retryable: true });
    }
    throw error;
  } finally {
    clearTimeout(timeout);
  }
}

async function buildContext(global) {
  const auth = await loadAuth(global.authStdin);
  return {
    server: (global.server ?? auth.server ?? DEFAULT_SERVER).replace(/\/+$/, ""),
    auth,
    scheme: global.scheme,
    timeout: Number(global.timeout ?? DEFAULT_TIMEOUT),
    requestId: global.requestId ?? crypto.randomUUID(),
  };
}

async function loadAuth(authStdin) {
  if (authStdin) {
    const raw = await new Response(Bun.stdin.stream()).text();
    return normalizeAuth(JSON.parse(raw || "{}"));
  }
  const extraHeaders = process.env.TJWATER_EXTRA_HEADERS
    ? JSON.parse(process.env.TJWATER_EXTRA_HEADERS)
    : {};
  return normalizeAuth({
    server: process.env.TJWATER_SERVER,
    access_token: process.env.TJWATER_ACCESS_TOKEN,
    project_id: process.env.TJWATER_PROJECT_ID,
    user_id: process.env.TJWATER_USER_ID,
    username: process.env.TJWATER_USERNAME,
    network: process.env.TJWATER_NETWORK,
    headers: extraHeaders,
  });
}

function normalizeAuth(raw) {
  return {
    server: pick(raw, "server", "base_url"),
    accessToken: pick(raw, "access_token", "token", "accessToken"),
    projectId: pick(raw, "project_id", "projectId", "x_project_id"),
    userId: pick(raw, "user_id", "userId", "x_user_id"),
    username: pick(raw, "username", "preferred_username"),
    network: pick(raw, "network", "project_code", "projectCode", "project"),
    headers: typeof raw.headers === "object" && raw.headers !== null ? raw.headers : {},
  };
}

function parseArgs(argv) {
  const global = {};
  const command = [];
  const args = {};
  const positionals = [];
  let help = false;
  let commandStarted = false;
  for (let i = 0; i < argv.length; i += 1) {
    const token = argv[i];
    if (token === "--help" || token === "-h") {
      help = true;
      continue;
    }
    if (!commandStarted && token.startsWith("--")) {
      const key = camel(token.slice(2));
      if (["authStdin"].includes(key)) {
        global[key] = true;
      } else {
        global[key] = argv[++i];
      }
      continue;
    }
    commandStarted = true;
    if (token.startsWith("--")) {
      const key = token.slice(2);
      const next = argv[i + 1];
      const value = next && !next.startsWith("--") ? argv[++i] : true;
      if (args[key] === undefined) {
        args[key] = value;
      } else if (Array.isArray(args[key])) {
        args[key].push(value);
      } else {
        args[key] = [args[key], value];
      }
      continue;
    }
    if (Object.keys(args).length === 0 && positionals.length === 0) {
      command.push(token);
    } else {
      positionals.push(token);
    }
  }
  return { global, command, args, positionals, help };
}

function requireOptions(args, spec) {
  for (const [name, option] of Object.entries(spec)) {
    if (option.required && (args[name] === undefined || args[name] === "")) {
      throw cliError("CLI 参数错误", "MISSING_OPTION", `missing required option --${name}`, 2);
    }
  }
}

function emitHelp(command = []) {
  const payload = {
    ok: true,
    schema_version: SCHEMA_VERSION,
    summary: command.length ? `命令帮助：${command.join(" ")}` : "TJWater agent CLI",
    data: {
      command: command.join(" "),
      groups: GROUPS,
      examples: [
        "tjwater-cli help",
        "tjwater-cli network get-all-pipes-properties",
        "tjwater-cli data timeseries realtime links --start-time 2025-01-01T00:00:00+08:00 --end-time 2025-01-01T01:00:00+08:00",
        "tjwater-cli analysis leakage schemes list",
      ],
    },
  };
  console.log(JSON.stringify(payload, null, 2));
}

function emitSuccess({ summary, data, ctx, durationMs, nextCommands }) {
  console.log(JSON.stringify({
    ok: true,
    schema_version: SCHEMA_VERSION,
    summary,
    data,
    metadata: {
      server: ctx.server,
      request_id: ctx.requestId,
      duration_ms: durationMs,
    },
    next_commands: nextCommands,
  }));
}

function emitFailure(error) {
  const payload = {
    ok: false,
    schema_version: SCHEMA_VERSION,
    summary: error.summary ?? "命令失败",
    error: {
      code: error.code ?? "UNKNOWN",
      message: error.message ?? String(error),
      retryable: Boolean(error.retryable),
    },
    data: error.data,
    next_commands: error.next_commands,
  };
  console.error(JSON.stringify(payload));
  return error.exitCode ?? 1;
}

function cliError(summary, code, message, exitCode, extra = {}) {
  const error = new Error(message);
  error.summary = summary;
  error.code = code;
  error.exitCode = exitCode;
  Object.assign(error, extra);
  return error;
}

function requireNetwork(ctx) {
  if (ctx.auth.network) return ctx.auth.network;
  throw cliError("认证失败", "NETWORK_CONTEXT_REQUIRED", "missing network in auth context", 3);
}

function requireProject(ctx) {
  if (ctx.auth.projectId) return ctx.auth.projectId;
  throw cliError("认证失败", "PROJECT_CONTEXT_REQUIRED", "missing project_id for agent context", 3);
}

function requireUsername(ctx) {
  if (ctx.auth.username) return ctx.auth.username;
  throw cliError("认证失败", "USERNAME_CONTEXT_REQUIRED", "missing username in auth context", 3);
}

function resolveScheme(ctx, explicit, required) {
  const scheme = explicit ?? ctx.scheme;
  if (required && !scheme) {
    throw cliError("CLI 参数错误", "SCHEME_REQUIRED", "missing scheme; use --scheme", 2);
  }
  return scheme;
}

function parseTime(value, optionName) {
  if (!value || Number.isNaN(Date.parse(value)) || !/[zZ]|[+-]\d\d:\d\d$/.test(value)) {
    throw cliError("CLI 参数错误", "INVALID_TIME", `${optionName} must be RFC3339 with timezone`, 2);
  }
  return value;
}

function requirePos(values, index, name) {
  const value = values[index];
  if (!value) throw cliError("CLI 参数错误", "MISSING_ARGUMENT", `missing ${name}`, 2);
  return value;
}

function pick(raw, ...keys) {
  for (const key of keys) {
    if (raw?.[key] !== undefined && raw[key] !== "") return raw[key];
  }
  return undefined;
}

function valueOf(value, args) {
  return typeof value === "function" ? value(args) : value;
}

function maybe(values) {
  return Object.fromEntries(Object.entries(values).filter(([, value]) => value !== undefined && value !== ""));
}

function toArray(value) {
  return Array.isArray(value) ? value : value === undefined ? [] : [value];
}

function camel(value) {
  return value.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
}

main()
  .then((code) => process.exit(code))
  .catch((error) => process.exit(emitFailure(error)));
