import { join, resolve } from "node:path";
import { DatabaseSync } from "node:sqlite";
import { BestAgentError, createRandomId } from "@best-agent/foundation";
import type {
  ApprovalCapabilityGroup,
  ApprovalDecision,
  ApprovalGrant,
  ApprovalGrantId,
  ApprovalGrantScope,
  CapabilityKind,
  ApprovalReviewMode,
  ApprovalId,
  ApprovalRequest,
  ApprovalReviewRecord,
  RiskClass,
  SandboxProfile,
  ThreadId,
  TurnId,
} from "@best-agent/protocol";

export interface ApprovalStoreOptions {
  storageRoot: string;
}

export type SaveApprovalGrantInput =
  | {
      action: string;
      group: ApprovalCapabilityGroup;
      kind: CapabilityKind;
      scope: "thread";
      threadId: ThreadId;
    }
  | {
      action: string;
      group: ApprovalCapabilityGroup;
      kind: CapabilityKind;
      scope: Exclude<ApprovalGrantScope, "once" | "thread">;
      threadId?: undefined;
    };

export interface ApprovalStore {
  close?(): void | Promise<void>;
  deleteGrantsForThread(threadId: ThreadId): Promise<void>;
  deleteReviewsForThread(threadId: ThreadId): Promise<void>;
  findGrant(group: ApprovalCapabilityGroup, kind: CapabilityKind, action: string, input?: { threadId?: ThreadId }): Promise<ApprovalGrant | null>;
  listGrants(): Promise<ApprovalGrant[]>;
  listReviews(threadId: ThreadId, limit?: number): Promise<ApprovalReviewRecord[]>;
  revokeGrant(grantId: ApprovalGrantId): Promise<void>;
  saveGrant(input: SaveApprovalGrantInput): Promise<ApprovalGrant>;
  saveReviewRequest(approval: ApprovalRequest): Promise<ApprovalReviewRecord>;
  settleReview(
    approvalId: ApprovalId,
    input: {
      decision?: ApprovalDecision;
      scope?: ApprovalGrantScope;
      status: ApprovalReviewRecord["status"];
    },
  ): Promise<ApprovalReviewRecord | null>;
}

type GrantRow = {
  id: string;
  scope: string;
  group_name: string;
  kind_name: string | null;
  action: string;
  created_at: string;
  thread_id: string | null;
};

type ReviewRow = {
  approval_id: string;
  thread_id: string;
  turn_id: string;
  group_name: string;
  kind_name: string;
  action: string;
  available_scopes: string;
  risk_class: string | null;
  review_mode: string | null;
  sandbox_profile: string | null;
  guardian_assessment: string | null;
  reason: string | null;
  requested_at: string;
  status: string;
  decision: string | null;
  scope: string | null;
  resolved_at: string | null;
};

export class LocalApprovalStore implements ApprovalStore {
  readonly #database: DatabaseSync;

  constructor(options: ApprovalStoreOptions) {
    this.#database = new DatabaseSync(join(resolve(options.storageRoot), "state.sqlite"));
    this.#database.exec(`
      PRAGMA journal_mode = WAL;
      PRAGMA busy_timeout = 2500;

      CREATE TABLE IF NOT EXISTS approval_grants (
        id TEXT PRIMARY KEY,
        scope TEXT NOT NULL,
        group_name TEXT NOT NULL,
        kind_name TEXT,
        action TEXT NOT NULL,
        thread_id TEXT,
        created_at TEXT NOT NULL
      );

      CREATE TABLE IF NOT EXISTS approval_reviews (
        approval_id TEXT PRIMARY KEY,
        thread_id TEXT NOT NULL,
        turn_id TEXT NOT NULL,
        group_name TEXT NOT NULL,
        kind_name TEXT NOT NULL,
        action TEXT NOT NULL,
        available_scopes TEXT NOT NULL,
        risk_class TEXT,
        review_mode TEXT,
        sandbox_profile TEXT,
        guardian_assessment TEXT,
        reason TEXT,
        requested_at TEXT NOT NULL,
        status TEXT NOT NULL,
        decision TEXT,
        scope TEXT,
        resolved_at TEXT
      );
    `);
    try {
      this.#database.exec(`ALTER TABLE approval_grants ADD COLUMN thread_id TEXT`);
    } catch {
      // Existing databases already have the column.
    }
    try {
      this.#database.exec(`ALTER TABLE approval_grants ADD COLUMN kind_name TEXT`);
    } catch {
      // Existing databases already have the column.
    }
    try {
      this.#database.exec(`ALTER TABLE approval_reviews ADD COLUMN risk_class TEXT`);
    } catch {
      // Existing databases already have the column.
    }
    try {
      this.#database.exec(`ALTER TABLE approval_reviews ADD COLUMN review_mode TEXT`);
    } catch {
      // Existing databases already have the column.
    }
    try {
      this.#database.exec(`ALTER TABLE approval_reviews ADD COLUMN sandbox_profile TEXT`);
    } catch {
      // Existing databases already have the column.
    }
    try {
      this.#database.exec(`ALTER TABLE approval_reviews ADD COLUMN guardian_assessment TEXT`);
    } catch {
      // Existing databases already have the column.
    }
    this.#database.exec(`
      DROP INDEX IF EXISTS approval_grants_scope_group_action_idx;

      CREATE UNIQUE INDEX IF NOT EXISTS approval_grants_scope_group_action_idx
        ON approval_grants(scope, group_name, IFNULL(kind_name, group_name), action, IFNULL(thread_id, ''));

      CREATE INDEX IF NOT EXISTS approval_reviews_thread_idx
        ON approval_reviews(thread_id, requested_at DESC, approval_id DESC);
    `);
  }

  close(): void {
    this.#database.close();
  }

  async deleteGrantsForThread(threadId: ThreadId): Promise<void> {
    this.#database.prepare(`DELETE FROM approval_grants WHERE thread_id = ?`).run(threadId);
  }

  async deleteReviewsForThread(threadId: ThreadId): Promise<void> {
    this.#database.prepare(`DELETE FROM approval_reviews WHERE thread_id = ?`).run(threadId);
  }

  async findGrant(
    group: ApprovalCapabilityGroup,
    kind: CapabilityKind,
    action: string,
    input: { threadId?: ThreadId } = {},
  ): Promise<ApprovalGrant | null> {
    if (input.threadId) {
      const threadRow = this.#database
        .prepare(
          `SELECT id, scope, group_name, kind_name, action, created_at, thread_id
           FROM approval_grants
           WHERE group_name = ? AND IFNULL(kind_name, group_name) = ? AND action = ? AND scope = 'thread' AND thread_id = ?
           ORDER BY created_at DESC, id DESC
           LIMIT 1`,
        )
        .get(group, kind, action, input.threadId) as GrantRow | undefined;
      if (threadRow) {
        return hydrateGrant(threadRow);
      }
    }

    const row = this.#database
      .prepare(
        `SELECT id, scope, group_name, kind_name, action, created_at, thread_id
         FROM approval_grants
         WHERE group_name = ? AND IFNULL(kind_name, group_name) = ? AND action = ? AND scope = 'workspace' AND thread_id IS NULL
         ORDER BY created_at DESC, id DESC
         LIMIT 1`,
      )
      .get(group, kind, action) as GrantRow | undefined;

    return row ? hydrateGrant(row) : null;
  }

  async listGrants(): Promise<ApprovalGrant[]> {
    const rows = this.#database
      .prepare(
        `SELECT id, scope, group_name, kind_name, action, created_at, thread_id
         FROM approval_grants
         ORDER BY created_at DESC, id DESC`,
      )
      .all() as GrantRow[];

    return rows.map(hydrateGrant);
  }

  async listReviews(threadId: ThreadId, limit = 50): Promise<ApprovalReviewRecord[]> {
    const rows = this.#database
      .prepare(
        `SELECT approval_id, thread_id, turn_id, group_name, kind_name, action, available_scopes, risk_class, review_mode, sandbox_profile, guardian_assessment, reason, requested_at, status, decision, scope, resolved_at
         FROM approval_reviews
         WHERE thread_id = ?
         ORDER BY requested_at DESC, approval_id DESC
         LIMIT ?`,
      )
      .all(threadId, limit) as ReviewRow[];

    return rows.map(hydrateReview);
  }

  async revokeGrant(grantId: ApprovalGrantId): Promise<void> {
    const result = this.#database.prepare(`DELETE FROM approval_grants WHERE id = ?`).run(grantId);
    if (result.changes === 0) {
      throw new BestAgentError("APPROVAL_GRANT_NOT_FOUND", `Approval grant ${grantId} not found`);
    }
  }

  async saveGrant(input: SaveApprovalGrantInput): Promise<ApprovalGrant> {
    const existing = await this.#findScopedGrant(input);
    if (existing && existing.scope === input.scope && existing.threadId === input.threadId) {
      return existing;
    }

    const grant: ApprovalGrant = {
      id: createRandomId<"ApprovalGrantId">("grant"),
      scope: input.scope,
      group: input.group,
      kind: input.kind,
      action: input.action,
      createdAt: new Date().toISOString(),
      threadId: input.scope === "thread" ? input.threadId : undefined,
    };

    this.#database
      .prepare(
        `INSERT INTO approval_grants (id, scope, group_name, kind_name, action, thread_id, created_at)
         VALUES (?, ?, ?, ?, ?, ?, ?)`,
      )
      .run(grant.id, grant.scope, grant.group, grant.kind, grant.action, grant.threadId ?? null, grant.createdAt);

    return grant;
  }

  async saveReviewRequest(approval: ApprovalRequest): Promise<ApprovalReviewRecord> {
    this.#database
      .prepare(
        `INSERT INTO approval_reviews (
          approval_id, thread_id, turn_id, group_name, kind_name, action, available_scopes, risk_class, review_mode, sandbox_profile, guardian_assessment, reason, requested_at, status, decision, scope, resolved_at
         )
         VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending', NULL, NULL, NULL)
         ON CONFLICT(approval_id) DO UPDATE SET
           thread_id = excluded.thread_id,
           turn_id = excluded.turn_id,
           group_name = excluded.group_name,
           kind_name = excluded.kind_name,
           action = excluded.action,
           available_scopes = excluded.available_scopes,
           risk_class = excluded.risk_class,
           review_mode = excluded.review_mode,
           sandbox_profile = excluded.sandbox_profile,
           guardian_assessment = excluded.guardian_assessment,
           reason = excluded.reason,
           requested_at = excluded.requested_at`,
      )
      .run(
        approval.id,
        approval.threadId,
        approval.turnId,
        approval.group,
        approval.kind,
        approval.action,
        JSON.stringify(approval.availableScopes),
        approval.riskClass,
        approval.reviewMode,
        approval.sandboxProfile,
        approval.guardianAssessment ? JSON.stringify(approval.guardianAssessment) : null,
        approval.reason ?? null,
        approval.requestedAt,
      );

    return {
      ...approval,
      status: "pending",
    };
  }

  async settleReview(
    approvalId: ApprovalId,
    input: {
      decision?: ApprovalDecision;
      scope?: ApprovalGrantScope;
      status: ApprovalReviewRecord["status"];
    },
  ): Promise<ApprovalReviewRecord | null> {
    const resolvedAt = new Date().toISOString();
    const result = this.#database
      .prepare(
        `UPDATE approval_reviews
         SET status = ?, decision = ?, scope = ?, resolved_at = ?
         WHERE approval_id = ?`,
      )
      .run(input.status, input.decision ?? null, input.scope ?? null, resolvedAt, approvalId);
    if (result.changes === 0) {
      return null;
    }

    const row = this.#database
      .prepare(
        `SELECT approval_id, thread_id, turn_id, group_name, kind_name, action, available_scopes, risk_class, review_mode, sandbox_profile, guardian_assessment, reason, requested_at, status, decision, scope, resolved_at
         FROM approval_reviews
         WHERE approval_id = ?`,
      )
      .get(approvalId) as ReviewRow | undefined;
    return row ? hydrateReview(row) : null;
  }

  async #findScopedGrant(input: SaveApprovalGrantInput): Promise<ApprovalGrant | null> {
    if (input.scope === "thread") {
      const row = this.#database
        .prepare(
          `SELECT id, scope, group_name, kind_name, action, created_at, thread_id
           FROM approval_grants
           WHERE group_name = ? AND IFNULL(kind_name, group_name) = ? AND action = ? AND scope = 'thread' AND thread_id = ?
           ORDER BY created_at DESC, id DESC
           LIMIT 1`,
        )
        .get(input.group, input.kind, input.action, input.threadId) as GrantRow | undefined;
      return row ? hydrateGrant(row) : null;
    }
    const row = this.#database
      .prepare(
        `SELECT id, scope, group_name, kind_name, action, created_at, thread_id
         FROM approval_grants
         WHERE group_name = ? AND IFNULL(kind_name, group_name) = ? AND action = ? AND scope = 'workspace' AND thread_id IS NULL
         ORDER BY created_at DESC, id DESC
         LIMIT 1`,
      )
      .get(input.group, input.kind, input.action) as GrantRow | undefined;
    return row ? hydrateGrant(row) : null;
  }
}

function hydrateGrant(row: GrantRow): ApprovalGrant {
  return {
    id: row.id as ApprovalGrantId,
    scope: row.scope as ApprovalGrant["scope"],
    group: row.group_name as ApprovalCapabilityGroup,
    kind: (row.kind_name ?? row.group_name) as CapabilityKind,
    action: row.action,
    createdAt: row.created_at,
    threadId: (row.thread_id as ThreadId | null) ?? undefined,
  };
}

function hydrateReview(row: ReviewRow): ApprovalReviewRecord {
  return {
    id: row.approval_id as ApprovalId,
    threadId: row.thread_id as ThreadId,
    turnId: row.turn_id as TurnId,
    group: row.group_name as ApprovalCapabilityGroup,
    kind: row.kind_name as CapabilityKind,
    action: row.action,
    availableScopes: JSON.parse(row.available_scopes) as ApprovalGrantScope[],
    riskClass: (row.risk_class ?? "high") as RiskClass,
    reviewMode: (row.review_mode ?? "guardian") as ApprovalReviewMode,
    sandboxProfile: (row.sandbox_profile ?? "none") as SandboxProfile,
    ...(row.guardian_assessment ? { guardianAssessment: JSON.parse(row.guardian_assessment) } : {}),
    ...(row.reason ? { reason: row.reason } : {}),
    requestedAt: row.requested_at,
    status: row.status as ApprovalReviewRecord["status"],
    ...(row.decision ? { decision: row.decision as ApprovalDecision } : {}),
    ...(row.scope ? { scope: row.scope as ApprovalGrantScope } : {}),
    ...(row.resolved_at ? { resolvedAt: row.resolved_at } : {}),
  };
}
