const crypto = require("crypto");
const { db } = require("./db");
const { checkCommandRequirement, labelRequirement } = require("./command-entitlements");
const { getProviderStreamIdentity } = require("./command-platform");
const { createLogger } = require("./logger");

const policyLog = createLogger("core:command-policies", { category: "command" });
const STREAM_RESTART_TOLERANCE_MS = 2 * 60 * 60 * 1000;
const DEFAULT_GROUP_ID = "core-default";
let defaultGroupEnsured = false;

function commandKey(pluginId, commandId) {
  return `premade:${String(pluginId || "core")}:${String(commandId || "command")}`;
}

function customCommandKey(id) {
  return `custom:${String(id)}`;
}

function listGroups() {
  ensureDefaultGroup();
  return db.prepare("SELECT * FROM command_groups ORDER BY name COLLATE NOCASE").all().map((row) => ({
    ...row,
    is_default: Boolean(row.is_default),
    policy: parsePolicy(row.policy_json)
  }));
}

function ensureDefaultGroup() {
  if (defaultGroupEnsured) return;
  const now = Date.now();
  db.prepare(
    "INSERT OR IGNORE INTO command_groups (id, name, description, policy_json, is_default, created_at, updated_at) VALUES (?, ?, ?, ?, 1, ?, ?)"
  ).run(
    DEFAULT_GROUP_ID,
    "All commands (defaults)",
    "Baseline settings inherited by every command unless that command overrides a category.",
    "{}",
    now,
    now
  );
  db.prepare("UPDATE command_groups SET is_default = 1 WHERE id = ?").run(DEFAULT_GROUP_ID);
  defaultGroupEnsured = true;
}

function createGroup({ name, description = "", policy = {} }) {
  const id = crypto.randomUUID();
  const now = Date.now();
  db.prepare("INSERT INTO command_groups (id, name, description, policy_json, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)")
    .run(id, cleanName(name), String(description || "").trim().slice(0, 500), JSON.stringify(normalizeStoredPolicy(policy)), now, now);
  return id;
}

function updateGroup(id, { name, description = "", policy = {} }) {
  db.prepare("UPDATE command_groups SET name = ?, description = ?, policy_json = ?, updated_at = ? WHERE id = ?")
    .run(cleanName(name), String(description || "").trim().slice(0, 500), JSON.stringify(normalizeStoredPolicy(policy)), Date.now(), id);
}

function deleteGroup(id) {
  ensureDefaultGroup();
  const group = db.prepare("SELECT is_default FROM command_groups WHERE id = ?").get(id);
  if (group?.is_default) throw new Error("The default command group cannot be deleted.");
  db.transaction(() => {
    db.prepare("DELETE FROM command_group_members WHERE group_id = ?").run(id);
    db.prepare("DELETE FROM command_groups WHERE id = ?").run(id);
  })();
}

function getCommandPolicy(commandKeyValue) {
  ensureDefaultGroup();
  const row = db.prepare("SELECT policy_json FROM command_policies WHERE command_key = ?").get(commandKeyValue);
  const groups = db.prepare(
    "SELECT g.* FROM command_groups g JOIN command_group_members m ON m.group_id = g.id WHERE m.command_key = ? AND g.is_default = 0 ORDER BY g.name COLLATE NOCASE"
  ).all(commandKeyValue).map((group) => ({ ...group, is_default: false, policy: parsePolicy(group.policy_json) }));
  return { policy: row ? parsePolicy(row.policy_json) : {}, groups };
}

function saveCommandPolicy(commandKeyValue, policy, groupIds = []) {
  saveCommandPolicies([{ commandKey: commandKeyValue, policy, groupIds }]);
}

function saveCommandPolicies(updates = []) {
  const now = Date.now();
  db.transaction(() => {
    const upsertPolicy = db.prepare(
      "INSERT INTO command_policies (command_key, policy_json, updated_at) VALUES (?, ?, ?) ON CONFLICT(command_key) DO UPDATE SET policy_json = excluded.policy_json, updated_at = excluded.updated_at"
    );
    const deletePolicy = db.prepare("DELETE FROM command_policies WHERE command_key = ?");
    const deleteMembers = db.prepare("DELETE FROM command_group_members WHERE command_key = ?");
    const insertMember = db.prepare("INSERT OR IGNORE INTO command_group_members (group_id, command_key, created_at) VALUES (?, ?, ?)");
    for (const update of updates) {
      const commandKeyValue = String(update?.commandKey || "");
      if (!commandKeyValue) throw new Error("Command key is required.");
      const normalized = normalizeStoredPolicy(update.policy);
      if (Object.keys(normalized).length) upsertPolicy.run(commandKeyValue, JSON.stringify(normalized), now);
      else deletePolicy.run(commandKeyValue);
      deleteMembers.run(commandKeyValue);
      for (const groupId of [...new Set((update.groupIds || []).map(String))]) {
        if (groupId !== DEFAULT_GROUP_ID) insertMember.run(groupId, commandKeyValue, now);
      }
    }
  })();
}

function resolvePolicy(commandKeyValue, defaultPolicy = {}, { ignoreDirect = [] } = {}) {
  const stored = getCommandPolicy(commandKeyValue);
  const ignored = new Set(ignoreDirect);
  const direct = Object.fromEntries(Object.entries(stored.policy).filter(([name]) => !ignored.has(name)));
  const groupPolicies = stored.groups.map((group) => ({ id: group.id, name: group.name, policy: group.policy }));
  const defaultRow = db.prepare("SELECT * FROM command_groups WHERE id = ?").get(DEFAULT_GROUP_ID);
  const globalDefault = defaultRow ? { id: defaultRow.id, name: defaultRow.name, policy: parsePolicy(defaultRow.policy_json) } : null;
  const defaults = normalizeStoredPolicy(defaultPolicy);
  return {
    windows: resolveRules(commandKeyValue, "window", direct, groupPolicies, globalDefault, defaults),
    perStreams: resolveRules(commandKeyValue, "perStream", direct, groupPolicies, globalDefault, defaults),
    requirements: resolveRequirements(direct, groupPolicies, globalDefault, defaults),
    cost: resolveCost(direct, groupPolicies, globalDefault, defaults),
    direct,
    groups: stored.groups,
    defaultGroup: globalDefault
  };
}

async function beginCommandUse({ commandKey: key, ctx, defaultPolicy = {} }) {
  const policy = resolvePolicy(key, defaultPolicy);
  for (const requirement of policy.requirements) {
    const checked = await checkCommandRequirement(ctx, requirement.value);
    if (!checked.allowed) {
      policyLog.info("Command requirement denied", {
        command_key: key,
        user_id: ctx.user?.id,
        platform: ctx.platform,
        requirement: requirement.value?.role,
        source: requirement.source,
        verification_error: checked.verificationError || null
      }, { event: "command_requirement_denied" });
      return { allowed: false, message: checked.reason };
    }
  }

  let streamSessionId = null;
  if (policy.perStreams.length) {
    streamSessionId = await resolveStreamSession(ctx);
    if (!streamSessionId) {
      return { allowed: false, message: "Lumi cannot identify the current stream, so this stream-limited command cannot be used right now." };
    }
  }

  const usageId = crypto.randomUUID();
  const now = Date.now();
  const userId = String(ctx.user?.id || "");
  let limitMessage = null;
  db.transaction(() => {
    db.prepare("DELETE FROM command_policy_uses WHERE used_at < ?").run(now - 45 * 24 * 60 * 60 * 1000);
    for (const rule of policy.windows) {
      const scopedUser = rule.value.scope === "global" ? null : userId;
      const count = db.prepare(
        "SELECT COUNT(*) AS count FROM command_policy_uses WHERE rule_key = ? AND user_id IS ? AND used_at >= ?"
      ).get(rule.key, scopedUser, now - rule.value.seconds * 1000)?.count || 0;
      if (count >= rule.value.uses) {
        limitMessage = rule.value.scope === "global"
          ? `That command has reached its shared limit. Try again in up to ${formatDuration(rule.value.seconds)}.`
          : `You have reached this command's limit. Try again in up to ${formatDuration(rule.value.seconds)}.`;
        return;
      }
    }
    for (const rule of policy.perStreams) {
      const scopedUser = rule.value.scope === "global" ? null : userId;
      const count = db.prepare(
        "SELECT COUNT(*) AS count FROM command_policy_uses WHERE rule_key = ? AND stream_session_id = ? AND user_id IS ?"
      ).get(rule.key, streamSessionId, scopedUser)?.count || 0;
      if (count >= rule.value.uses) {
        limitMessage = rule.value.scope === "global"
          ? "That command has reached its limit for this stream."
          : "You have reached this command's limit for this stream.";
        return;
      }
    }
    const insert = db.prepare(
      "INSERT INTO command_policy_uses (id, command_key, rule_key, user_id, stream_session_id, used_at) VALUES (?, ?, ?, ?, ?, ?)"
    );
    for (const rule of policy.windows) {
      insert.run(usageId, key, rule.key, rule.value.scope === "global" ? null : userId, null, now);
    }
    for (const rule of policy.perStreams) {
      insert.run(usageId, key, rule.key, rule.value.scope === "global" ? null : userId, streamSessionId, now);
    }
  })();
  if (limitMessage) return { allowed: false, message: limitMessage };

  let charged = false;
  if (policy.cost > 0) {
    const economy = global.lumiFrameworks?.economy;
    if (!economy?.removeBalance) {
      releaseUsage(usageId);
      return { allowed: false, message: "This command has a currency cost, but the Economy Framework is not available." };
    }
    let result;
    try {
      result = economy.removeBalance({
        userId,
        amount: policy.cost,
        note: `Command cost: ${ctx.raw || key}`,
        meta: { source: "command_cost", commandKey: key, trigger: ctx.trigger, platform: ctx.platform }
      });
    } catch (error) {
      releaseUsage(usageId);
      policyLog.error("Command cost charge failed", { command_key: key, user_id: userId, amount: policy.cost, error }, { event: "command_cost_failed" });
      return { allowed: false, message: "The command cost could not be charged." };
    }
    if (!result?.ok) {
      releaseUsage(usageId);
      return { allowed: false, message: result?.message === "Insufficient balance."
        ? `You need ${policy.cost} currency to use this command.`
        : (result?.message || "The command cost could not be charged.") };
    }
    charged = true;
  }

  let finished = false;
  return {
    allowed: true,
    async commit() { finished = true; },
    async rollback(reason = "Command did not complete") {
      if (finished) return;
      finished = true;
      releaseUsage(usageId);
      if (charged) refundCommandCost({ userId, amount: policy.cost, key, ctx, reason });
    }
  };
}

function releaseUsage(usageId) {
  db.prepare("DELETE FROM command_policy_uses WHERE id = ?").run(usageId);
}

function refundCommandCost({ userId, amount, key, ctx, reason }) {
  try {
    global.lumiFrameworks?.economy?.addBalance?.({
      userId,
      amount,
      note: `Command cost refund: ${ctx.raw || key}`,
      meta: { source: "command_cost_refund", commandKey: key, reason }
    });
  } catch (error) {
    policyLog.error("Command cost refund failed", { command_key: key, user_id: userId, amount, error }, { event: "command_cost_refund_failed" });
  }
}

async function resolveStreamSession(ctx) {
  const identity = await getProviderStreamIdentity(ctx);
  if (!identity?.streamKey) return null;
  const now = Date.now();
  const existing = db.prepare("SELECT * FROM command_stream_sessions WHERE stream_key = ?").get(identity.streamKey);
  const sameProviderStream = existing && identity.providerStreamId && existing.provider_stream_id === identity.providerStreamId;
  const withinRestartTolerance = existing && now - existing.last_seen_at <= STREAM_RESTART_TOLERANCE_MS;
  const sessionId = sameProviderStream || withinRestartTolerance ? existing.session_id : crypto.randomUUID();
  const startedAt = sessionId === existing?.session_id ? existing.started_at : now;
  db.prepare(
    "INSERT INTO command_stream_sessions (stream_key, session_id, provider_stream_id, started_at, last_seen_at) VALUES (?, ?, ?, ?, ?) " +
    "ON CONFLICT(stream_key) DO UPDATE SET session_id = excluded.session_id, provider_stream_id = excluded.provider_stream_id, started_at = excluded.started_at, last_seen_at = excluded.last_seen_at"
  ).run(identity.streamKey, sessionId, identity.providerStreamId || existing?.provider_stream_id || null, startedAt, now);
  return sessionId;
}

function resolveRules(commandKeyValue, category, direct, groups, globalDefault, defaults) {
  if (Object.prototype.hasOwnProperty.call(direct, category)) {
    return direct[category] === false ? [] : [{ key: `${commandKeyValue}:${category}`, source: "command", value: direct[category] }];
  }
  const rules = groups.flatMap((group) => {
    const value = group.policy[category];
    return value && value !== false ? [{ key: `group:${group.id}:${category}`, source: group.name, value }] : [];
  });
  const defaultHasCategory = Boolean(globalDefault && Object.prototype.hasOwnProperty.call(globalDefault.policy, category));
  const defaultValue = defaultHasCategory ? globalDefault.policy[category] : undefined;
  if (defaultValue && defaultValue !== false) {
    rules.push({ key: `group:${globalDefault.id}:${category}`, source: globalDefault.name, value: defaultValue });
  }
  if (rules.length) return rules;
  if (defaultHasCategory && defaultValue === false) return [];
  const fallback = defaults[category];
  return fallback && fallback !== false ? [{ key: `${commandKeyValue}:default:${category}`, source: "default", value: fallback }] : [];
}

function resolveRequirements(direct, groups, globalDefault, defaults) {
  if (Object.prototype.hasOwnProperty.call(direct, "requirement")) {
    return direct.requirement === false ? [] : [{ source: "command", value: direct.requirement }];
  }
  const values = groups.flatMap((group) => group.policy.requirement && group.policy.requirement !== false
    ? [{ source: group.name, value: group.policy.requirement }]
    : []);
  const defaultHasRequirement = Boolean(globalDefault && Object.prototype.hasOwnProperty.call(globalDefault.policy, "requirement"));
  const defaultRequirement = defaultHasRequirement ? globalDefault.policy.requirement : undefined;
  if (defaultRequirement && defaultRequirement !== false) values.push({ source: globalDefault.name, value: defaultRequirement });
  if (values.length) return values;
  if (defaultHasRequirement && defaultRequirement === false) return [];
  return defaults.requirement && defaults.requirement !== false ? [{ source: "default", value: defaults.requirement }] : [];
}

function resolveCost(direct, groups, globalDefault, defaults) {
  if (Object.prototype.hasOwnProperty.call(direct, "cost")) return direct.cost === false ? 0 : Number(direct.cost?.amount || 0);
  const costs = groups.map((group) => Number(group.policy.cost?.amount || 0)).filter((amount) => amount > 0);
  const defaultHasCost = Boolean(globalDefault && Object.prototype.hasOwnProperty.call(globalDefault.policy, "cost"));
  const defaultCost = defaultHasCost ? Number(globalDefault.policy.cost?.amount || 0) : 0;
  if (defaultCost > 0) costs.push(defaultCost);
  if (costs.length) return Math.max(...costs);
  if (defaultHasCost && globalDefault.policy.cost === false) return 0;
  return Number(defaults.cost?.amount || 0);
}

function normalizeStoredPolicy(value) {
  const input = value && typeof value === "object" ? value : {};
  const output = {};
  if (Object.prototype.hasOwnProperty.call(input, "window")) output.window = normalizeWindow(input.window);
  if (Object.prototype.hasOwnProperty.call(input, "perStream")) output.perStream = normalizePerStream(input.perStream);
  if (Object.prototype.hasOwnProperty.call(input, "requirement")) output.requirement = normalizeRequirement(input.requirement);
  if (Object.prototype.hasOwnProperty.call(input, "cost")) output.cost = normalizeCost(input.cost);
  return Object.fromEntries(Object.entries(output).filter(([, item]) => item !== undefined));
}

function normalizeWindow(value) {
  if (value === false || value?.enabled === false) return false;
  if (!value || typeof value !== "object") return undefined;
  return {
    uses: clampInteger(value.uses, 1, 100000),
    seconds: clampInteger(value.seconds, 1, 31_536_000),
    scope: value.scope === "global" ? "global" : "user"
  };
}

function normalizePerStream(value) {
  if (value === false || value?.enabled === false) return false;
  if (!value || typeof value !== "object") return undefined;
  return { uses: clampInteger(value.uses, 1, 100000), scope: value.scope === "global" ? "global" : "user" };
}

function normalizeRequirement(value) {
  if (value === false || value?.enabled === false || value?.role === "public") return false;
  const role = ["follower", "subscriber", "vip", "mod", "editor", "streamer"].includes(value?.role) ? value.role : null;
  if (!role) return undefined;
  return { role, subscriberTier: role === "subscriber" ? clampInteger(value.subscriberTier, 1, 3) : 1 };
}

function normalizeCost(value) {
  if (value === false || value?.enabled === false || Number(value?.amount) <= 0) return false;
  if (!value || typeof value !== "object") return undefined;
  return { amount: clampInteger(value.amount, 1, 1_000_000_000) };
}

function parsePolicy(value) {
  try { return normalizeStoredPolicy(JSON.parse(value || "{}")); } catch { return {}; }
}

function cleanName(value) {
  const name = String(value || "").trim().slice(0, 100);
  if (!name) throw new Error("Group name is required.");
  return name;
}

function clampInteger(value, minimum, maximum) {
  return Math.min(maximum, Math.max(minimum, Math.round(Number(value) || minimum)));
}

function formatDuration(seconds) {
  if (seconds < 60) return `${seconds} second${seconds === 1 ? "" : "s"}`;
  const minutes = Math.ceil(seconds / 60);
  return `${minutes} minute${minutes === 1 ? "" : "s"}`;
}

module.exports = {
  DEFAULT_GROUP_ID,
  STREAM_RESTART_TOLERANCE_MS,
  beginCommandUse,
  commandKey,
  createGroup,
  customCommandKey,
  deleteGroup,
  ensureDefaultGroup,
  getCommandPolicy,
  labelRequirement,
  listGroups,
  normalizeStoredPolicy,
  resolvePolicy,
  saveCommandPolicy,
  saveCommandPolicies,
  updateGroup
};
