import { KdlStructuredError } from "../../kdl/rpc.js";
import type { MotionDiagnostic, MotionSourceMap } from "../../kdl/types.js";
import type { GrlProcedureDeclaration, GrlRawTopLevelDeclaration, GrlTopLevelDeclaration } from "../ast/index.js";
import type {
  AlarmInstruction,
  ExceptionFlowInstruction,
  RaiseInstruction,
  RawProcedureStatement,
  TryInstruction,
  UnsupportedRuntimeInstruction
} from "../ir/index.js";
import type { GrlToken } from "../lexer/index.js";

type StopKeyword = "catch" | "finally" | "end";

export interface ExceptionAnalysis {
  procedures: Record<string, ExceptionFlowInstruction[]>;
  unsupported: UnsupportedRuntimeInstruction[];
  diagnostics: MotionDiagnostic[];
}

export function analyzeExceptionSemantics(declarations: GrlTopLevelDeclaration[]): ExceptionAnalysis {
  const diagnostics: MotionDiagnostic[] = [];
  const procedures: Record<string, ExceptionFlowInstruction[]> = {};
  const unsupported: UnsupportedRuntimeInstruction[] = [];

  for (const declaration of declarations) {
    if (declaration.kind === "ProcedureDeclaration") {
      procedures[declaration.name] = parseProcedureExceptionFlow(declaration);
      continue;
    }
    if (declaration.kind === "RawTopLevelDeclaration") {
      const instruction = compileUnsupportedTopLevel(declaration);
      if (instruction) {
        unsupported.push(instruction);
        diagnostics.push(diagnostic("warning", "GRL_P1_UNIMPLEMENTED", instruction.message, instruction.sourceMap));
      }
    }
  }

  return { procedures, unsupported, diagnostics };
}

export function parseProcedureExceptionFlow(procedure: GrlProcedureDeclaration): ExceptionFlowInstruction[] {
  return parseExceptionFlowStatements(procedure.bodyTokens);
}

export function parseExceptionFlowStatements(tokens: GrlToken[]): ExceptionFlowInstruction[] {
  return new ExceptionFlowParser(tokens).parseRoot();
}

class ExceptionFlowParser {
  private current = 0;

  constructor(private readonly tokens: GrlToken[]) {}

  parseRoot(): ExceptionFlowInstruction[] {
    return this.parseBlock(new Set());
  }

  private parseBlock(stopKeywords: Set<StopKeyword>): ExceptionFlowInstruction[] {
    const instructions: ExceptionFlowInstruction[] = [];
    while (!this.isAtEnd()) {
      if (this.isStopKeyword(stopKeywords)) {
        break;
      }
      if (this.matchKeyword("alarm")) {
        instructions.push(this.finishAlarm(this.previous()));
        continue;
      }
      if (this.matchKeyword("raise")) {
        instructions.push(this.finishRaise(this.previous()));
        continue;
      }
      if (this.matchKeyword("try")) {
        instructions.push(this.finishTry(this.previous()));
        continue;
      }
      if (this.matchKeyword("enable") || this.matchKeyword("disable")) {
        instructions.push(this.finishUnsupportedInterrupt(this.previous()));
        continue;
      }
      instructions.push(this.finishRawStatement());
    }
    return instructions;
  }

  private finishAlarm(start: GrlToken): AlarmInstruction {
    const tokens = this.collectLineTokens(start);
    const id = tokens[0];
    if (!id || !isIdentifierLike(id)) {
      throw exceptionError("GRL_ALARM_ID_MISSING", "alarm requires an alarm id", tokenSourceMap(start));
    }
    const message = tokens.find((token) => token.kind === "string");
    const severityIndex = tokens.findIndex((token) => token.raw === "severity");
    const severity = severityIndex >= 0 ? tokens[severityIndex + 1] : undefined;
    return {
      kind: "ALARM",
      alarmId: id.raw,
      ...(message?.kind === "string" ? { message: message.value } : {}),
      ...(severity && isIdentifierLike(severity) ? { severity: severity.raw } : {}),
      sourceMap: tokenSourceMap(start)
    };
  }

  private finishRaise(start: GrlToken): RaiseInstruction {
    const tokens = this.collectLineTokens(start);
    const id = tokens[0];
    if (!id || !isIdentifierLike(id)) {
      throw exceptionError("GRL_RAISE_ID_MISSING", "raise requires an alarm id", tokenSourceMap(start));
    }
    return {
      kind: "RAISE",
      alarmId: id.raw,
      sourceMap: tokenSourceMap(start)
    };
  }

  private finishTry(start: GrlToken): TryInstruction {
    const body = this.parseBlock(new Set(["catch", "finally", "end"]));
    const catches: TryInstruction["catches"] = [];
    let finallyBlock: TryInstruction["finally"];

    while (this.matchKeyword("catch")) {
      const catchStart = this.previous();
      const header = this.collectLineTokens(catchStart);
      const alarmId = header[0] && isIdentifierLike(header[0]) ? header[0].raw : undefined;
      catches.push({
        ...(alarmId ? { alarmId } : {}),
        body: this.parseBlock(new Set(["catch", "finally", "end"])),
        sourceMap: tokenSourceMap(catchStart)
      });
    }

    if (this.matchKeyword("finally")) {
      const finallyStart = this.previous();
      finallyBlock = {
        body: this.parseBlock(new Set(["end"])),
        sourceMap: tokenSourceMap(finallyStart)
      };
    }

    this.consumeKeyword("end", "Expected end after try block");
    if (catches.length === 0 && !finallyBlock) {
      throw exceptionError("GRL_TRY_HANDLER_MISSING", "try requires catch or finally", tokenSourceMap(start));
    }

    return {
      kind: "TRY",
      body,
      catches,
      ...(finallyBlock ? { finally: finallyBlock } : {}),
      sourceMap: tokenSourceMap(start)
    };
  }

  private finishUnsupportedInterrupt(start: GrlToken): UnsupportedRuntimeInstruction {
    const tokens = this.collectLineTokens(start);
    const hasInterrupt = tokens.some((token) => token.raw === "interrupt");
    return {
      kind: "UNSUPPORTED_RUNTIME",
      feature: "interrupt",
      message: hasInterrupt
        ? `${start.raw} interrupt is parsed but not executable in P0`
        : `${start.raw} is parsed but not executable in P0`,
      sourceMap: tokenSourceMap(start)
    };
  }

  private finishRawStatement(): RawProcedureStatement {
    const start = this.peek();
    const tokens = this.collectLineTokens(start);
    if (tokens.length === 0) {
      const token = this.advance();
      return {
        kind: "RAW_STATEMENT",
        text: token.raw,
        tokens: [token],
        sourceMap: tokenSourceMap(token)
      };
    }
    return {
      kind: "RAW_STATEMENT",
      text: stringifyTokens(tokens),
      tokens,
      sourceMap: tokenSourceMap(start)
    };
  }

  private collectLineTokens(start: GrlToken): GrlToken[] {
    const tokens: GrlToken[] = [];
    let parenDepth = 0;
    let bracketDepth = 0;
    let braceDepth = 0;
    while (!this.isAtEnd()) {
      const token = this.peek();
      if (token.range.start.line !== start.range.start.line) {
        break;
      }
      const consumed = this.advance();
      tokens.push(consumed);
      if (consumed.kind === "punctuation") {
        if (consumed.raw === "(") parenDepth += 1;
        if (consumed.raw === ")") parenDepth = Math.max(0, parenDepth - 1);
        if (consumed.raw === "[") bracketDepth += 1;
        if (consumed.raw === "]") bracketDepth = Math.max(0, bracketDepth - 1);
        if (consumed.raw === "{") braceDepth += 1;
        if (consumed.raw === "}") braceDepth = Math.max(0, braceDepth - 1);
      }
      if (parenDepth === 0 && bracketDepth === 0 && braceDepth === 0) {
        continue;
      }
    }
    return tokens;
  }

  private isStopKeyword(stopKeywords: Set<StopKeyword>): boolean {
    if (stopKeywords.size === 0) {
      return false;
    }
    const token = this.peek();
    return (token.kind === "keyword" || token.kind === "identifier") && stopKeywords.has(token.raw as StopKeyword);
  }

  private matchKeyword(keyword: string): boolean {
    if (this.checkKeyword(keyword)) {
      this.advance();
      return true;
    }
    return false;
  }

  private checkKeyword(keyword: string): boolean {
    const token = this.peek();
    return (token.kind === "keyword" || token.kind === "identifier") && token.raw === keyword;
  }

  private consumeKeyword(keyword: string, message: string): GrlToken {
    if (this.checkKeyword(keyword)) {
      return this.advance();
    }
    throw exceptionError("GRL_KEYWORD_EXPECTED", message, tokenSourceMap(this.peek()));
  }

  private advance(): GrlToken {
    this.current += 1;
    return this.previous();
  }

  private previous(): GrlToken {
    return this.tokens[this.current - 1]!;
  }

  private peek(): GrlToken {
    return this.tokens[this.current]!;
  }

  private isAtEnd(): boolean {
    return this.current >= this.tokens.length;
  }
}

function compileUnsupportedTopLevel(declaration: GrlRawTopLevelDeclaration): UnsupportedRuntimeInstruction | undefined {
  if (declaration.declarationType === "trap") {
    return {
      kind: "UNSUPPORTED_RUNTIME",
      feature: "trap",
      message: `trap ${declaration.tokens[1]?.raw ?? ""}`.trim() + " is parsed but not executable in P0",
      sourceMap: rangeSourceMap(declaration.range.start)
    };
  }
  if (declaration.declarationType === "task") {
    return {
      kind: "UNSUPPORTED_RUNTIME",
      feature: "task",
      message: `task ${declaration.tokens[1]?.raw ?? ""}`.trim() + " is parsed but not executable in P0",
      sourceMap: rangeSourceMap(declaration.range.start)
    };
  }
  return undefined;
}

function isIdentifierLike(token: GrlToken): boolean {
  return token.kind === "identifier" || token.kind === "keyword";
}

function stringifyTokens(tokens: GrlToken[]): string {
  return tokens.map((token) => token.raw).join(" ");
}

function tokenSourceMap(token: GrlToken): MotionSourceMap {
  return {
    line: token.range.start.line,
    column: token.range.start.column
  };
}

function rangeSourceMap(position: { line: number; column: number }): MotionSourceMap {
  return {
    line: position.line,
    column: position.column
  };
}

function diagnostic(
  severity: MotionDiagnostic["severity"],
  code: string,
  message: string,
  sourceMap?: MotionSourceMap
): MotionDiagnostic {
  return {
    severity,
    code,
    message,
    ...(sourceMap ? { sourceMap } : {})
  };
}

function exceptionError(code: string, message: string, sourceMap: MotionSourceMap): KdlStructuredError {
  return new KdlStructuredError(code, message, [diagnostic("error", code, message, sourceMap)]);
}
