// Copyright (C) 2026 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import {
  LONG,
  LONG_NULL,
  NUM,
  NUM_NULL,
  STR,
  STR_NULL,
} from '../../trace_processor/query_result';
import type {Trace} from '../../public/trace';
import type {PerfettoPlugin} from '../../public/plugin';
import {SliceTrack} from '../../components/tracks/slice_track';
import {SourceDataset} from '../../trace_processor/dataset';
import {TrackNode} from '../../public/workspace';
import {Time} from '../../base/time';
import type {App} from '../../public/app';
import type {RouteArgs} from '../../public/route_schema';
import ProcessThreadGroupsPlugin from '../dev.perfetto.ProcessThreadGroups';
import {AnrDetailsPanel} from './details_panel';
import {findMainThreadTrackUri, scrollToTrackAndSelect} from './navigate';

const ANR_TRACK_URI = '/android_anrs';

interface AnrArgs {
  processName?: string;
  packageName?: string;
  errorId?: string;
  autoSelect?: boolean; // true if the base plugin id 'com.android.AndroidAnr' is present in the route args
}

function getAnrArgsFromRouteArgs(args: RouteArgs): AnrArgs {
  const tempArgs: AnrArgs = {autoSelect: false};

  const baseKey = AndroidAnr.id;
  const processNameKey = baseKey + '.processName';
  const errorIdKey = baseKey + '.errorId';
  const packageNameKey = baseKey + '.packageName';

  const processName = args[processNameKey];
  if (typeof processName === 'string') {
    tempArgs.processName = processName;
  }

  const packageName = args[packageNameKey];
  if (typeof packageName === 'string') {
    tempArgs.packageName = packageName;
  }

  const errorId = args[errorIdKey];
  if (typeof errorId === 'string') {
    tempArgs.errorId = errorId;
  }

  // Default behaviour: if the flag '${AndroidAnr.id}' is the ONLY argument
  // then auto-select the last ANR.
  if (args.hasOwnProperty(baseKey)) {
    tempArgs.autoSelect = true;
  }

  return tempArgs;
}

let anrArgs: AnrArgs;

export default class AndroidAnr implements PerfettoPlugin {
  static readonly id = 'com.android.AndroidAnr';
  static readonly dependencies = [ProcessThreadGroupsPlugin];

  static onActivate(app: App): void {
    const args: RouteArgs = app.initialRouteArgs;
    anrArgs = getAnrArgsFromRouteArgs(args);
  }

  async onTraceLoad(ctx: Trace): Promise<void> {
    const e = ctx.engine;
    await e.query(`
      include perfetto module android.anrs;
    `);

    const cnt = await e.query('select count() cnt from android_anrs');
    if (cnt.firstRow({cnt: LONG}).cnt === 0n) {
      return;
    }

    ctx.tracks.registerTrack({
      uri: ANR_TRACK_URI,
      renderer: await SliceTrack.createMaterialized({
        trace: ctx,
        uri: ANR_TRACK_URI,
        dataset: new SourceDataset({
          schema: {
            ts: LONG,
            dur: LONG_NULL,
            name: STR,
            upid: NUM_NULL,
            process_name: STR,
            pid: NUM_NULL,
            anr_type: STR,
            subject: STR_NULL,
          },
          src: `
            SELECT
              ts - coalesce(anr_dur_ms, default_anr_dur_ms, 0) * 1000000 AS ts,
              coalesce(anr_dur_ms, default_anr_dur_ms, 0) * 1000000 AS dur,
              process_name || ' ' || pid || ' : ' || anr_type AS name,
              upid,
              process_name,
              pid,
              anr_type,
              subject
            FROM android_anrs
          `,
        }),
        detailsPanel: () => new AnrDetailsPanel(ctx),
      }),
    });

    // Needs a sort order lower than 'Ftrace Events' so that it is prioritized in the UI.
    const anrTrack = new TrackNode({
      name: 'Android ANRs',
      uri: ANR_TRACK_URI,
      sortOrder: -6,
    });
    ctx.defaultWorkspace.addChildInOrder(anrTrack);

    await this.selectAnr(ctx, anrArgs);
  }

  private pinAnrTrack(ctx: Trace) {
    const trackNode = ctx.currentWorkspace.getTrackByUri(ANR_TRACK_URI);
    if (trackNode) {
      trackNode.pin();
    }
  }

  private async selectAnr(ctx: Trace, args: AnrArgs) {
    const e = ctx.engine;

    const whereFilters = [];
    if (args.processName !== undefined) {
      whereFilters.push(`anr.process_name = '${args.processName}'`);
    }
    if (args.packageName !== undefined) {
      whereFilters.push(`apm.package_name = '${args.packageName}'`);
    }
    if (args.errorId !== undefined) {
      whereFilters.push(`anr.error_id = '${args.errorId}'`);
    }

    // Order by descending ts to get the last anr first
    const orderByClause = 'ORDER BY anr.ts DESC';
    let whereClause = '';

    if (whereFilters.length > 0) {
      whereClause = 'WHERE ' + whereFilters.join(' AND ');
    } else if (!args.autoSelect) {
      return;
    }

    await e.query(`
      include perfetto module android.process_metadata;
    `);

    const query = `
      SELECT
        anr.ts - coalesce(anr.anr_dur_ms, anr.default_anr_dur_ms) * 1000000 AS ts,
        coalesce(anr.anr_dur_ms, anr.default_anr_dur_ms) * 1000000 AS dur,
        anr.upid,
        tt.id AS main_thread_track_id
      FROM
        android_anrs anr
      JOIN
        android_process_metadata apm ON anr.upid = apm.upid
      LEFT JOIN
        thread t ON anr.upid = t.upid AND t.is_main_thread = 1
      LEFT JOIN
        thread_track tt ON t.utid = tt.utid
      ${whereClause}
      ${orderByClause}
      LIMIT 1;
    `;

    const result = await e.query(query);
    const it = result.iter({
      ts: LONG,
      dur: LONG_NULL,
      upid: NUM,
      main_thread_track_id: NUM_NULL,
    });

    if (!it.valid()) {
      return;
    }

    const anrInfo = {
      ts: it.ts,
      dur: it.dur ?? 0n,
      upid: it.upid,
      mainThreadTrackId: it.main_thread_track_id,
    };

    this.pinAnrTrack(ctx);

    const startTime = Time.fromRaw(BigInt(anrInfo.ts));

    ctx.onTraceReady.addListener(async () => {
      const group = (
        ctx.plugins.getPlugin(
          ProcessThreadGroupsPlugin,
        ) as ProcessThreadGroupsPlugin
      ).getGroupForProcess(anrInfo.upid);

      if (!group?.uri) {
        return;
      }
      group.expand();

      const tracksToSelect: string[] = [];
      if (anrInfo.mainThreadTrackId !== null) {
        const uri = findMainThreadTrackUri(ctx, anrInfo.mainThreadTrackId);
        if (uri) {
          tracksToSelect.push(uri);
        }
      }

      scrollToTrackAndSelect(
        ctx,
        group.uri,
        tracksToSelect,
        startTime,
        anrInfo.dur,
      );
    });
  }
}
