/**
 * MCP Tool: analyze_legislative_effectiveness
 *
 * Score an MEP's or committee's legislative output from real EP Open Data
 * Portal sources: procedures (rapporteur attribution), adopted texts
 * (success rate), plenary-session document items (amendments tabled/adopted),
 * and parliamentary questions (questions asked).
 *
 * **Intelligence Perspective:** Combines four EP API sources behind a single
 * resilient fan-out so analysts get defensible per-MEP / per-committee
 * effectiveness scores even when one upstream endpoint is slow or down.
 *
 * **ISMS Policy:** SC-002 (Input Validation), AC-003 (Least Privilege),
 * AU-002 (Audit Logging).
 *
 * @see Hack23/ISMS-PUBLIC Secure Development Policy §4.10 (Data Quality)
 * @see ISO 27001 Annex A.8.11; GDPR Article 5(1)(d)
 */

import { AnalyzeLegislativeEffectivenessSchema } from '../schemas/europeanParliament.js';
import { epClient } from '../clients/europeanParliamentClient.js';
import { buildToolResponse } from './shared/responseBuilder.js';
import type { ToolResult } from './shared/types.js';
import { auditLogger, toErrorMessage } from '../utils/auditLogger.js';
import { withTimeoutAndAbort, TimeoutError } from '../utils/timeout.js';
import {
  aggregateLegislativeEffectiveness,
  roundToTwoDecimals,
  type LegislativeMetrics,
  type AggregationResult,
} from '../utils/effectivenessAggregator.js';
import type { Procedure, AdoptedText } from '../types/ep/activities.js';
import type { LegislativeDocument } from '../types/ep/document.js';
import type { ParliamentaryQuestion } from '../types/ep/question.js';

// ---------------------------------------------------------------------------
// Public-facing types
// ---------------------------------------------------------------------------

/**
 * Per-source data availability flag surfaced in the response envelope.
 *
 * - `OK` — fetch succeeded and at least one item contributed to the metric.
 * - `EMPTY` — fetch succeeded but no items matched the subject/date filter.
 * - `TIMEOUT` — per-source 6 s budget expired; metric reported as zero.
 * - `UNAVAILABLE` — EP API returned an error.
 */
export type DataSourceStatus = 'OK' | 'EMPTY' | 'TIMEOUT' | 'UNAVAILABLE';

/**
 * Per-source availability block emitted in the response.
 */
export interface DataSources {
  procedures: DataSourceStatus;
  adoptedTexts: DataSourceStatus;
  plenaryDocumentItems: DataSourceStatus;
  questions: DataSourceStatus;
}

interface LegislativeScores {
  productivityScore: number;
  qualityScore: number;
  impactScore: number;
  overallEffectiveness: number;
}

interface LegislativeEffectivenessAnalysis {
  subjectType: string;
  subjectId: string;
  subjectName: string;
  period: { from: string; to: string };
  metrics: LegislativeMetrics;
  scores: LegislativeScores;
  computedAttributes: {
    amendmentSuccessRate: number;
    legislativeOutputPerMonth: number;
    avgImpactPerReport: number;
    questionFollowUpRate: number;
    committeeCoverageRate: number;
    peerComparisonPercentile: number;
    effectivenessRank: string;
  };
  benchmarks: { avgReportsPerMep: number; avgAmendmentsPerMep: number; avgSuccessRate: number };
  attributions: AggregationResult['attributions'];
  dataSources: DataSources;
  confidenceLevel: 'HIGH' | 'MEDIUM' | 'LOW';
  dataFreshness: string;
  sourceAttribution: string;
  methodology: string;
  dataQualityWarnings: string[];
  cacheHit?: boolean;
}

// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------

/** Per-source timeout budget (issue spec: 6 s each). */
const SOURCE_TIMEOUT_MS = 6_000;
/** Page size used for client-side filtering of bulk endpoints. */
const FETCH_PAGE_LIMIT = 200;
/**
 * Maximum number of pages walked by `fetchProcedures()` per request.
 * Bounds the worst-case cost at `MAX_PROCEDURE_PAGES * FETCH_PAGE_LIMIT`
 * items so a deep result set cannot starve the 6 s per-source budget or
 * exhaust the 100 req/min EP API rate-limit token bucket.
 */
const MAX_PROCEDURE_PAGES = 5;
/** 15-minute cache TTL per (subjectType, subjectId, dateFrom, dateTo). */
const CACHE_TTL_MS = 15 * 60 * 1000;
/** Bound the in-memory analysis cache. */
const MAX_CACHE_ENTRIES = 200;

// ---------------------------------------------------------------------------
// Cache
// ---------------------------------------------------------------------------

interface CacheEntry {
  expiresAt: number;
  value: LegislativeEffectivenessAnalysis;
}

const analysisCache = new Map<string, CacheEntry>();

function makeCacheKey(subjectType: string, subjectId: string, dateFrom: string, dateTo: string): string {
  return `${subjectType.toUpperCase()}|${subjectId.toUpperCase()}|${dateFrom}|${dateTo}`;
}

function readCache(key: string): LegislativeEffectivenessAnalysis | undefined {
  const entry = analysisCache.get(key);
  if (entry === undefined) return undefined;
  if (entry.expiresAt <= Date.now()) {
    analysisCache.delete(key);
    return undefined;
  }
  return { ...entry.value, cacheHit: true };
}

function writeCache(key: string, value: LegislativeEffectivenessAnalysis): void {
  if (analysisCache.size >= MAX_CACHE_ENTRIES) {
    const firstKey = analysisCache.keys().next().value;
    if (firstKey !== undefined) analysisCache.delete(firstKey);
  }
  analysisCache.set(key, {
    expiresAt: Date.now() + CACHE_TTL_MS,
    value: { ...value, cacheHit: false },
  });
}

/**
 * Clear the analyze_legislative_effectiveness cache (test-only helper).
 *
 * @internal
 */
export function clearAnalyzeLegislativeEffectivenessCache(): void {
  analysisCache.clear();
}

// ---------------------------------------------------------------------------
// Source result modelling
// ---------------------------------------------------------------------------

interface SourceFetchResult<T> {
  status: DataSourceStatus;
  items: T[];
  error?: string;
  /**
   * Optional human-readable warnings emitted by the fetcher when partial
   * data was returned (e.g. one year of a multi-year /adopted-texts fan-out
   * failed, or `fetchProcedures` truncated pagination at the page cap).
   * Surfaced in `dataQualityWarnings` so downstream tools can branch on
   * partial coverage rather than treating the result as fully complete.
   */
  warnings?: string[];
}

/**
 * Per-source fetch payload: items plus any partial-coverage warnings the
 * fetcher wants to surface (e.g. "year 2024 fetch failed" or "pagination
 * truncated at page cap").
 */
interface FetcherResult<T> {
  items: T[];
  warnings?: string[];
}

/**
 * Run a per-source fetch under the 6 s budget, mapping outcomes into a
 * `DataSourceStatus`. Per-source failures never bubble — callers see a
 * structured envelope so unrelated sources continue to populate values.
 *
 * The fetcher may return partial-coverage `warnings` (e.g. when one year of
 * a multi-year fan-out fails but others succeed); these are forwarded to
 * the response envelope without changing the source's `OK` status.
 */
async function runSource<T>(
  source: keyof DataSources,
  fetcher: (signal: AbortSignal) => Promise<FetcherResult<T>>,
): Promise<SourceFetchResult<T>> {
  const startedAt = Date.now();
  try {
    const result = await withTimeoutAndAbort(
      fetcher,
      SOURCE_TIMEOUT_MS,
      `analyze_legislative_effectiveness:${source} timed out after ${String(SOURCE_TIMEOUT_MS)}ms`,
    );
    const items = result.items;
    const count = items.length;
    auditLogger.logDataAccess(
      `analyze_legislative_effectiveness.${source}`,
      { source, truncated: count > 1000 ? '1000+' : count },
      count,
      Date.now() - startedAt,
    );
    const out: SourceFetchResult<T> = {
      status: count > 0 ? 'OK' : 'EMPTY',
      items,
    };
    if (result.warnings !== undefined && result.warnings.length > 0) {
      out.warnings = result.warnings;
    }
    return out;
  } catch (error: unknown) {
    const message = toErrorMessage(error);
    const isTimeout = error instanceof TimeoutError;
    auditLogger.logError(
      `analyze_legislative_effectiveness.${source}`,
      { source, timedOut: isTimeout },
      message,
      Date.now() - startedAt,
    );
    return {
      status: isTimeout ? 'TIMEOUT' : 'UNAVAILABLE',
      items: [],
      error: message,
    };
  }
}

// ---------------------------------------------------------------------------
// Per-source fetchers
// ---------------------------------------------------------------------------

/**
 * Throw if the provided AbortSignal has already been triggered.
 * This short-circuits the EP client call when the per-source timeout has
 * already fired (e.g. due to scheduling delays), preventing unnecessary
 * network requests. Note: the underlying BaseEPClient.get() does not yet
 * accept an external AbortSignal, so once a fetch starts it will run to
 * completion or its own 60 s timeout; this guard prevents *starting* the
 * request when the budget has already expired.
 */
function throwIfAborted(signal: AbortSignal): void {
  if (signal.aborted) {
    throw new TimeoutError('Per-source timeout already expired before EP client call');
  }
}

async function fetchProcedures(): Promise<SourceFetchResult<Procedure>> {
  return runSource<Procedure>('procedures', async (signal) => {
    throwIfAborted(signal);
    const aggregated: Procedure[] = [];
    let offset = 0;
    let page = 0;
    let exhausted = false;
    while (page < MAX_PROCEDURE_PAGES) {
      throwIfAborted(signal);
      const resp = await epClient.getProcedures({ limit: FETCH_PAGE_LIMIT, offset, abortSignal: signal });
      const items = Array.isArray(resp.data) ? resp.data : [];
      aggregated.push(...items);
      page += 1;
      if (!resp.hasMore) {
        exhausted = true;
        break;
      }
      // Advance the offset by what the page actually returned, falling back
      // to the requested limit when the API returned an empty page with
      // `hasMore=true` (a rare EP API pattern observed when a page contains
      // only items filtered out by the API tier — defensive forward
      // progress prevents an infinite-loop while the page cap remains the
      // ultimate safeguard).
      offset += items.length > 0 ? items.length : FETCH_PAGE_LIMIT;
    }
    if (exhausted) {
      return { items: aggregated };
    }
    // Reached the page cap with `hasMore` still true — surface a warning so
    // downstream tools know `reportsAuthored`/`opinionsDelivered` and the
    // success-rate denominator may undercount.
    return {
      items: aggregated,
      warnings: [
        `procedures pagination truncated at ${String(MAX_PROCEDURE_PAGES)} pages `
        + `(${String(MAX_PROCEDURE_PAGES * FETCH_PAGE_LIMIT)} items) — `
        + 'reports/opinions/success-rate may undercount for broad windows',
      ],
    };
  });
}

/**
 * Extract calendar years inclusive between `dateFrom` and `dateTo`. Bounded
 * at 5 years to keep the per-source budget realistic; longer windows fall
 * back to an unfiltered fetch (year=undefined) so the caller still gets
 * partial coverage rather than no data.
 */
function yearsInWindow(dateFrom: string, dateTo: string): number[] {
  const fromYear = parseYear(dateFrom);
  const toYear = parseYear(dateTo);
  if (fromYear === undefined || toYear === undefined) return [];
  if (toYear < fromYear) return [];
  const span = toYear - fromYear + 1;
  if (span > 5) return [];
  const years: number[] = [];
  for (let y = fromYear; y <= toYear; y += 1) years.push(y);
  return years;
}

function parseYear(date: string): number | undefined {
  if (date.length < 4) return undefined;
  const year = parseInt(date.slice(0, 4), 10);
  if (!Number.isFinite(year) || year < 1900 || year >= 3000) return undefined;
  return year;
}

async function fetchAdoptedTexts(
  dateFrom: string,
  dateTo: string,
): Promise<SourceFetchResult<AdoptedText>> {
  return runSource<AdoptedText>('adoptedTexts', async (signal) => {
    throwIfAborted(signal);
    const years = yearsInWindow(dateFrom, dateTo);
    if (years.length === 0) {
      // Either invalid dates or a span > 5 years — fetch unfiltered and let
      // the aggregator filter by `dateAdopted` against the window.
      const resp = await epClient.getAdoptedTexts({ limit: FETCH_PAGE_LIMIT, abortSignal: signal });
      return { items: Array.isArray(resp.data) ? resp.data : [] };
    }
    // Parallel per-year fetches via `Promise.allSettled` so a single failing
    // year does not zero out the others — partial coverage is reported as a
    // warning rather than a full `UNAVAILABLE` status. Each task carries its
    // own `year` so the index/array bookkeeping stays explicit and the
    // failure list is built without index gymnastics.
    const settledByYear = await Promise.all(
      years.map(async (year): Promise<
        { year: number; ok: true; data: readonly AdoptedText[] }
        | { year: number; ok: false }
      > => {
        try {
          const resp = await epClient.getAdoptedTexts({ year, limit: FETCH_PAGE_LIMIT, abortSignal: signal });
          return { year, ok: true, data: Array.isArray(resp.data) ? resp.data : [] };
        } catch (err: unknown) {
          // Intentional suppression: per-year failures are surfaced via the
          // aggregated `failedYears` warning rather than thrown — partial
          // outage must not zero out the other years. The error is audit-
          // logged at the per-source level by `runSource()` if every year
          // ends up failing.
          void err;
          return { year, ok: false };
        }
      }),
    );
    const seen = new Set<string>();
    const merged: AdoptedText[] = [];
    const failedYears: number[] = [];
    for (const result of settledByYear) {
      if (!result.ok) {
        failedYears.push(result.year);
        continue;
      }
      for (const item of result.data) {
        if (seen.has(item.id)) continue;
        seen.add(item.id);
        merged.push(item);
      }
    }
    // If every year failed, propagate the failure so the source is marked
    // UNAVAILABLE rather than EMPTY.
    if (failedYears.length === years.length) {
      throw new Error(
        `adopted-texts fetch failed for all ${String(years.length)} year(s) in window`,
      );
    }
    const warnings: string[] = [];
    if (failedYears.length > 0) {
      warnings.push(
        `adopted-texts fetch failed for year(s) ${failedYears.join(', ')} — `
        + 'legislativeSuccessRate may undercount procedures adopted in those years',
      );
    }
    return warnings.length > 0 ? { items: merged, warnings } : { items: merged };
  });
}

async function fetchPlenaryDocumentItems(): Promise<SourceFetchResult<LegislativeDocument>> {
  return runSource<LegislativeDocument>('plenaryDocumentItems', async (signal) => {
    throwIfAborted(signal);
    const resp = await epClient.getPlenarySessionDocumentItems({ limit: FETCH_PAGE_LIMIT, abortSignal: signal });
    return { items: Array.isArray(resp.data) ? resp.data : [] };
  });
}

async function fetchQuestions(
  authorId: string | undefined,
  dateFrom: string,
  dateTo: string,
): Promise<SourceFetchResult<ParliamentaryQuestion>> {
  return runSource<ParliamentaryQuestion>('questions', async (signal) => {
    throwIfAborted(signal);
    const resp = await epClient.getParliamentaryQuestions({
      ...(authorId !== undefined ? { author: authorId } : {}),
      dateFrom,
      dateTo,
      limit: FETCH_PAGE_LIMIT,
      abortSignal: signal,
    });
    return { items: Array.isArray(resp.data) ? resp.data : [] };
  });
}

// ---------------------------------------------------------------------------
// Classification helpers
// ---------------------------------------------------------------------------

function classifyEffectivenessRank(score: number): string {
  if (score >= 70) return 'HIGHLY_EFFECTIVE';
  if (score >= 50) return 'EFFECTIVE';
  if (score >= 30) return 'MODERATE';
  return 'DEVELOPING';
}

function classifyConfidence(dataSources: DataSources, hasAnyData: boolean): 'HIGH' | 'MEDIUM' | 'LOW' {
  const okCount = (Object.values(dataSources) as DataSourceStatus[])
    .filter((s) => s === 'OK').length;
  if (okCount >= 3) return 'HIGH';
  if (hasAnyData) return 'MEDIUM';
  return 'LOW';
}

function computeScores(metrics: LegislativeMetrics): LegislativeScores {
  // Productivity: report + amendment volume, saturated at 100.
  const productivityScore = Math.min(
    100,
    metrics.reportsAuthored * 8
      + metrics.amendmentsTabled * 2
      + metrics.opinionsDelivered * 4,
  );
  // Quality: success rate combined with amendment adoption ratio.
  const amendmentSuccess = metrics.amendmentsTabled > 0
    ? (metrics.amendmentsAdopted / metrics.amendmentsTabled) * 100
    : 0;
  const qualityScore = Math.min(
    100,
    metrics.legislativeSuccessRate * 0.6 + amendmentSuccess * 0.4,
  );
  // Impact: reports + opinions + questions weighted by influence.
  const impactScore = Math.min(
    100,
    metrics.reportsAuthored * 10
      + metrics.opinionsDelivered * 5
      + Math.min(metrics.questionsAsked, 50) * 0.5,
  );
  const overallEffectiveness = roundToTwoDecimals(
    productivityScore * 0.35 + qualityScore * 0.35 + impactScore * 0.30,
  );
  return {
    productivityScore: roundToTwoDecimals(productivityScore),
    qualityScore: roundToTwoDecimals(qualityScore),
    impactScore: roundToTwoDecimals(impactScore),
    overallEffectiveness,
  };
}

function monthsBetween(dateFrom: string, dateTo: string): number {
  const from = new Date(dateFrom);
  const to = new Date(dateTo);
  if (Number.isNaN(from.getTime()) || Number.isNaN(to.getTime())) return 1;
  const ms = Math.max(0, to.getTime() - from.getTime());
  const days = ms / (1000 * 60 * 60 * 24);
  return Math.max(1, days / 30);
}

function buildQualityWarnings(dataSources: DataSources): string[] {
  const warnings: string[] = [];
  const labels: Record<keyof DataSources, string> = {
    procedures: 'procedures',
    adoptedTexts: 'adopted texts',
    plenaryDocumentItems: 'plenary-session document items',
    questions: 'parliamentary questions',
  };
  for (const key of ['procedures', 'adoptedTexts', 'plenaryDocumentItems', 'questions'] as const) {
    const status = dataSources[key];
    if (status === 'TIMEOUT') {
      warnings.push(`${labels[key]} fetch timed out after ${String(SOURCE_TIMEOUT_MS)}ms — metric reported as zero`);
    } else if (status === 'UNAVAILABLE') {
      warnings.push(`${labels[key]} unavailable from EP API — metric reported as zero`);
    } else if (status === 'EMPTY') {
      warnings.push(`${labels[key]} returned no items matching the subject within the date window`);
    }
  }
  return warnings;
}

const METHODOLOGY = (
  'Legislative effectiveness scoring built from four EP Open Data Portal sources fanned out '
  + `under a ${String(SOURCE_TIMEOUT_MS)}ms per-source timeout (Promise.allSettled): `
  + '/procedures (rapporteur / shadow-rapporteur / opinion-rapporteur attribution), '
  + '/adopted-texts (success-rate denominator), '
  + '/plenary-session-documents-items (amendments tabled + adoption flag from document status), '
  + '/parliamentary-questions (questions filed, filtered by author and date). '
  + 'For subjectType=COMMITTEE the aggregator sums per-MEP attributions across the committee '
  + 'membership returned by /corporate-bodies. legislativeSuccessRate = '
  + 'procedures-with-adopted-text / attributed-procedures (0-100). Each metric carries a per-source '
  + 'dataAvailability flag so downstream tools can branch on real coverage. Data source: '
  + 'European Parliament Open Data Portal (data.europarl.europa.eu).'
);

// ---------------------------------------------------------------------------
// Subject resolution
// ---------------------------------------------------------------------------

interface SubjectInfo {
  subjectName: string;
  /** Bare MEP id (used as the `author` query parameter for questions). */
  authorParam: string;
  /** Optional list of committee member MEP IDs (for committee aggregation). */
  committeeMemberIds?: string[];
}

/**
 * Normalise an MEP identifier into the bare token that matches the EP API's
 * `q.author` field for `/parliamentary-questions` (typically `person/{id}`).
 *
 * The question client applies the `author` filter client-side via
 * case-insensitive substring match against `q.author`, so the safest
 * portable token is the trailing non-empty segment after the last `/` or
 * `-` delimiter:
 *
 *  - `person/124810` → `124810`
 *  - `MEP-124810`    → `124810`
 *  - `124810`        → `124810`
 *
 * Returning the bare id makes the filter robust to whichever upstream
 * formatting the EP API uses (`person/{id}` is the documented shape).
 * Trailing delimiters are tolerated — the function returns the original
 * trimmed input rather than an empty string.
 *
 * @internal
 */
export function normaliseQuestionAuthorParam(subjectId: string): string {
  const trimmed = subjectId.trim();
  if (trimmed === '') return '';
  // Take the trailing non-empty segment after the last `/` or `-`. If the
  // input ends with a delimiter the segment is empty, so fall back to the
  // original trimmed value rather than returning ''. `findLast` short-
  // circuits without allocating an intermediate filtered array.
  const tail = trimmed.split(/[/-]/).findLast((seg) => seg !== '');
  return tail ?? trimmed;
}

async function resolveMepSubject(subjectId: string): Promise<SubjectInfo> {
  const mep = await epClient.getMEPDetails(subjectId);
  return {
    subjectName: mep.name,
    // EP question authors are returned as `person/{id}`; normalise the
    // incoming subjectId to the bare numeric/final-segment id so callers
    // can pass `MEP-124810`, `person/124810`, or `124810` interchangeably
    // and the client-side substring filter still matches.
    authorParam: normaliseQuestionAuthorParam(subjectId),
  };
}

async function resolveCommitteeSubject(subjectId: string): Promise<SubjectInfo> {
  const committee = await epClient.getCommitteeInfo({ abbreviation: subjectId });
  const members = Array.isArray(committee.members) ? committee.members : [];
  return {
    subjectName: committee.name,
    // EP API has no `committee` author filter for questions; we fetch
    // questions unfiltered and filter client-side via the aggregator's
    // committee-member token set.
    authorParam: '',
    committeeMemberIds: members,
  };
}

function resolveDefaultDateWindow(
  dateFrom: string | undefined,
  dateTo: string | undefined,
): { dateFrom: string; dateTo: string } {
  const now = new Date();
  const to = dateTo ?? (now.toISOString().split('T')[0] ?? '2024-12-31');
  const start = new Date(now.getFullYear(), 0, 1);
  const from = dateFrom ?? (start.toISOString().split('T')[0] ?? '2024-01-01');
  return { dateFrom: from, dateTo: to };
}

// ---------------------------------------------------------------------------
// Build envelope
// ---------------------------------------------------------------------------

interface BuildArgs {
  subjectType: 'MEP' | 'COMMITTEE';
  subjectId: string;
  dateFrom: string;
  dateTo: string;
}

async function fetchAllSources(args: BuildArgs, subject: SubjectInfo): Promise<{
  procedures: SourceFetchResult<Procedure>;
  adoptedTexts: SourceFetchResult<AdoptedText>;
  plenaryDocumentItems: SourceFetchResult<LegislativeDocument>;
  questions: SourceFetchResult<ParliamentaryQuestion>;
}> {
  const [procedures, adoptedTexts, plenaryDocumentItems, questions] = await Promise.all([
    fetchProcedures(),
    fetchAdoptedTexts(args.dateFrom, args.dateTo),
    fetchPlenaryDocumentItems(),
    fetchQuestions(
      args.subjectType === 'MEP' ? subject.authorParam : undefined,
      args.dateFrom,
      args.dateTo,
    ),
  ]);
  return { procedures, adoptedTexts, plenaryDocumentItems, questions };
}

/**
 * Refine OK-statused sources to EMPTY when none of their items contributed
 * to the subject — keeps `dataQualityWarnings` honest for downstream tools.
 */
function refineDataSources(dataSources: DataSources, aggregation: AggregationResult): void {
  if (dataSources.procedures === 'OK'
    && aggregation.metrics.reportsAuthored === 0
    && aggregation.metrics.opinionsDelivered === 0) {
    dataSources.procedures = 'EMPTY';
  }
  if (dataSources.adoptedTexts === 'OK' && aggregation.proceduresWithAdoptedText === 0) {
    dataSources.adoptedTexts = 'EMPTY';
  }
  if (dataSources.plenaryDocumentItems === 'OK' && aggregation.metrics.amendmentsTabled === 0) {
    dataSources.plenaryDocumentItems = 'EMPTY';
  }
  if (dataSources.questions === 'OK' && aggregation.metrics.questionsAsked === 0) {
    dataSources.questions = 'EMPTY';
  }
}

function computeComputedAttributes(
  args: BuildArgs,
  aggregation: AggregationResult,
  scores: LegislativeScores,
): LegislativeEffectivenessAnalysis['computedAttributes'] {
  const months = monthsBetween(args.dateFrom, args.dateTo);
  const metrics = aggregation.metrics;
  const outputPerMonth = roundToTwoDecimals(
    (metrics.reportsAuthored + metrics.amendmentsTabled) / months,
  );
  const avgImpact = metrics.reportsAuthored > 0
    ? roundToTwoDecimals(scores.impactScore / metrics.reportsAuthored)
    : 0;
  const amendmentSuccessRate = metrics.amendmentsTabled > 0
    ? roundToTwoDecimals((metrics.amendmentsAdopted / metrics.amendmentsTabled) * 100)
    : 0;
  const percentile = Math.min(99, Math.round(scores.overallEffectiveness * 1.1));
  return {
    amendmentSuccessRate,
    legislativeOutputPerMonth: outputPerMonth,
    avgImpactPerReport: avgImpact,
    // Question follow-up rate is a placeholder: the EP API does not expose
    // answer metadata at aggregate scale, so we report 100% when at least one
    // question is on file (questions reach published status by construction).
    questionFollowUpRate: metrics.questionsAsked > 0 ? 100 : 0,
    committeeCoverageRate: args.subjectType === 'COMMITTEE' ? 100 : 0,
    peerComparisonPercentile: percentile,
    effectivenessRank: classifyEffectivenessRank(scores.overallEffectiveness),
  };
}

async function buildAnalysis(args: BuildArgs): Promise<LegislativeEffectivenessAnalysis> {
  const subject: SubjectInfo = args.subjectType === 'MEP'
    ? await resolveMepSubject(args.subjectId)
    : await resolveCommitteeSubject(args.subjectId);

  const sources = await fetchAllSources(args, subject);

  const dataSources: DataSources = {
    procedures: sources.procedures.status,
    adoptedTexts: sources.adoptedTexts.status,
    plenaryDocumentItems: sources.plenaryDocumentItems.status,
    questions: sources.questions.status,
  };

  const aggregation = aggregateLegislativeEffectiveness({
    subjectId: args.subjectId,
    subjectName: subject.subjectName,
    ...(subject.committeeMemberIds !== undefined ? { committeeMemberIds: subject.committeeMemberIds } : {}),
    dateFrom: args.dateFrom,
    dateTo: args.dateTo,
    procedures: sources.procedures.items,
    adoptedTexts: sources.adoptedTexts.items,
    plenaryDocumentItems: sources.plenaryDocumentItems.items,
    questions: sources.questions.items,
  });

  refineDataSources(dataSources, aggregation);

  const scores = computeScores(aggregation.metrics);
  const computedAttributes = computeComputedAttributes(args, aggregation, scores);

  const hasAnyData = aggregation.metrics.reportsAuthored
    + aggregation.metrics.opinionsDelivered
    + aggregation.metrics.amendmentsTabled
    + aggregation.metrics.questionsAsked
    + aggregation.proceduresWithAdoptedText > 0;

  return {
    subjectType: args.subjectType,
    subjectId: args.subjectId,
    subjectName: subject.subjectName,
    period: { from: args.dateFrom, to: args.dateTo },
    metrics: aggregation.metrics,
    scores,
    computedAttributes,
    benchmarks: { avgReportsPerMep: 3.2, avgAmendmentsPerMep: 12.5, avgSuccessRate: 38.0 },
    attributions: aggregation.attributions,
    dataSources,
    confidenceLevel: classifyConfidence(dataSources, hasAnyData),
    dataFreshness: 'Real-time EP API data — procedures, adopted texts, plenary document items, parliamentary questions',
    sourceAttribution: 'European Parliament Open Data Portal - data.europarl.europa.eu',
    methodology: METHODOLOGY,
    dataQualityWarnings: [
      ...buildQualityWarnings(dataSources),
      ...(sources.procedures.warnings ?? []),
      ...(sources.adoptedTexts.warnings ?? []),
      ...(sources.plenaryDocumentItems.warnings ?? []),
      ...(sources.questions.warnings ?? []),
    ],
  };
}

// ---------------------------------------------------------------------------
// Public handler
// ---------------------------------------------------------------------------

/**
 * Handles the `analyze_legislative_effectiveness` MCP tool request.
 *
 * Fans out four EP Open Data Portal sources in parallel under independent
 * 6 s timeouts, then aggregates the results through
 * {@link aggregateLegislativeEffectiveness} to produce defensible
 * effectiveness metrics for an MEP or committee.
 *
 * @param args - Raw tool arguments, validated against
 *   {@link AnalyzeLegislativeEffectivenessSchema}
 * @returns MCP tool result with a {@link LegislativeEffectivenessAnalysis}
 *   envelope that includes metrics, scores, per-source `dataSources` flags,
 *   `dataQualityWarnings`, and full attribution lists sorted ascending.
 *
 * @example
 * ```typescript
 * const result = await handleAnalyzeLegislativeEffectiveness({
 *   subjectType: 'MEP',
 *   subjectId: 'person/124936',
 *   dateFrom: '2024-01-01',
 *   dateTo: '2024-12-31',
 * });
 * ```
 *
 * @security Input is validated with Zod before any API call. Per-source
 *   fetches are audit-logged with truncated counts (no MEP names beyond IDs).
 *   Per-source 6 s timeouts use AbortController to prevent dangling fetches.
 * @performance Warm cache: <300 ms. Cold worst case: ≈6-8 s (parallel
 *   sources). 15-minute cache keyed by (subjectType, subjectId, dateFrom,
 *   dateTo).
 * @since 0.8.0
 * @see {@link analyzeLegislativeEffectivenessToolMetadata}
 */
export async function handleAnalyzeLegislativeEffectiveness(
  args: unknown,
): Promise<ToolResult> {
  const params = AnalyzeLegislativeEffectivenessSchema.parse(args);
  const { dateFrom, dateTo } = resolveDefaultDateWindow(params.dateFrom, params.dateTo);
  const cacheKey = makeCacheKey(params.subjectType, params.subjectId, dateFrom, dateTo);

  const cached = readCache(cacheKey);
  if (cached !== undefined) {
    return buildToolResponse(cached);
  }

  try {
    const analysis = await buildAnalysis({
      subjectType: params.subjectType,
      subjectId: params.subjectId,
      dateFrom,
      dateTo,
    });
    writeCache(cacheKey, analysis);
    return buildToolResponse({ ...analysis, cacheHit: false });
  } catch (error: unknown) {
    const errorMessage = error instanceof Error ? error.message : 'Unknown error';
    throw new Error(`Failed to analyze legislative effectiveness: ${errorMessage}`);
  }
}

/**
 * Tool metadata for MCP registration.
 */
export const analyzeLegislativeEffectivenessToolMetadata = {
  name: 'analyze_legislative_effectiveness',
  description:
    'Analyze legislative effectiveness of an MEP or committee using real EP Open Data Portal sources. '
    + 'Computes reports authored, opinions delivered, amendments tabled/adopted, questions asked, '
    + 'and legislativeSuccessRate by fanning out /procedures, /adopted-texts, '
    + '/plenary-session-documents-items, and /parliamentary-questions under independent 6s timeouts. '
    + 'For subjectType=COMMITTEE, aggregates per-MEP across the committee membership.',
  inputSchema: {
    type: 'object' as const,
    properties: {
      subjectType: {
        type: 'string',
        enum: ['MEP', 'COMMITTEE'],
        description: 'Subject type to analyze',
      },
      subjectId: {
        type: 'string',
        description: 'Subject identifier (MEP ID or committee abbreviation)',
        minLength: 1,
        maxLength: 100,
      },
      dateFrom: {
        type: 'string',
        description: 'Analysis start date (YYYY-MM-DD format)',
        pattern: '^\\d{4}-\\d{2}-\\d{2}$',
      },
      dateTo: {
        type: 'string',
        description: 'Analysis end date (YYYY-MM-DD format)',
        pattern: '^\\d{4}-\\d{2}-\\d{2}$',
      },
    },
    required: ['subjectType', 'subjectId'],
  },
};
