import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
import path from 'path';
import { Story } from 'inkjs';
import {
  ChoiceResult,
  StoryTag,
  TurnResult,
} from '../interfaces/turn-result';
import { getTagValue, parseTags } from '../utils/tag-parser';

const { Compiler } = require('inkjs/full') as { Compiler: new (
  inkSource: string,
  options?: {
    sourceFilename?: string;
    errorHandler?: (message: string, type: number) => void;
    fileHandler?: {
      ResolveInkFilename: (filename: string) => string;
      LoadInkFileContents: (filename: string) => string;
    };
  },
) => { Compile: () => { ToJson: () => string } | null } };

export interface InkCompileResult {
  sourcePath: string;
  outputPath: string;
  warningCount: number;
}

export function compileInkSource(sourcePath: string, outputPath: string): InkCompileResult {
  const resolvedSource = path.resolve(sourcePath);
  const resolvedOutput = path.resolve(outputPath);
  if (!existsSync(resolvedSource)) {
    throw new Error(`Ink source file not found: ${resolvedSource}`);
  }

  const warnings: string[] = [];
  const errors: string[] = [];
  const source = readFileSync(resolvedSource, 'utf8').replace(/^\uFEFF/, '');
  const sourceDir = path.dirname(resolvedSource);
  const fileHandler = {
    ResolveInkFilename: (filename: string) =>
      path.isAbsolute(filename) ? filename : path.resolve(sourceDir, filename),
    LoadInkFileContents: (filename: string) =>
      readFileSync(path.isAbsolute(filename) ? filename : path.resolve(sourceDir, filename), 'utf8')
        .replace(/^\uFEFF/, ''),
  };
  const compiler = new Compiler(source, {
    sourceFilename: resolvedSource,
    fileHandler,
    errorHandler: (message: string, type: number) => {
      if (type === 1) {
        warnings.push(message);
      } else {
        errors.push(message);
      }
    },
  });

  const story = compiler.Compile();
  if (!story || errors.length > 0) {
    throw new Error(`Ink compilation failed:\n${errors.join('\n')}`);
  }

  if (warnings.length > 0) {
    warnings.forEach((warning) => console.warn(`[ink] ${warning}`));
  }

  mkdirSync(path.dirname(resolvedOutput), { recursive: true });
  writeFileSync(resolvedOutput, story.ToJson(), 'utf8');
  return {
    sourcePath: resolvedSource,
    outputPath: resolvedOutput,
    warningCount: warnings.length,
  };
}

export class InkEngine {
  private story: Story | null = null;
  private nextTurnId = 1;

  constructor(private readonly storyPath: string) {}

  isRunning(): boolean {
    if (!this.story) return false;
    return this.story.canContinue || this.story.currentChoices.length > 0;
  }

  newGame(): TurnResult {
    this.story = this.loadStory();
    this.nextTurnId = 1;
    return this.continueStory();
  }

  chooseChoice(choiceIndex: number): TurnResult {
    if (!this.story) {
      throw new Error('No active Ink story');
    }

    const choice = this.story.currentChoices.find((item) => item.index === choiceIndex);
    if (!choice) {
      throw new Error(`Ink choice ${choiceIndex} is not available`);
    }

    this.story.ChooseChoiceIndex(choice.index);
    return this.continueStory();
  }

  saveGame(): string {
    if (!this.story) {
      throw new Error('No active Ink story to save');
    }
    return JSON.stringify({
      inkState: this.story.state.toJson(),
      nextTurnId: this.nextTurnId,
    });
  }

  loadGame(savedState: string): TurnResult {
    this.story = this.loadStory();
    let inkState = savedState;
    try {
      const parsed = JSON.parse(savedState);
      if (parsed && typeof parsed.inkState === 'string') {
        inkState = parsed.inkState;
        if (Number.isInteger(parsed.nextTurnId)) {
          this.nextTurnId = Math.max(1, parsed.nextTurnId);
        }
      }
    } catch {
      // Backward compatibility with raw Ink state JSON.
    }
    this.story.state.LoadJson(inkState);
    return this.continueStory();
  }

  private loadStory(): Story {
    const resolvedPath = path.resolve(this.storyPath);
    if (!existsSync(resolvedPath)) {
      throw new Error(`Ink story file not found: ${resolvedPath}`);
    }

    const storyJson = JSON.parse(readFileSync(resolvedPath, 'utf8'));
    return new Story(storyJson);
  }

  private continueStory(): TurnResult {
    if (!this.story) {
      throw new Error('No active Ink story');
    }

    const paragraphs: TurnResult['paragraphs'] = [];
    const globalTags: StoryTag[] = [];
    const turnTags: StoryTag[] = [];

    while (this.story.canContinue) {
      const rawText = this.story.Continue();
      const text = String(rawText || '').trim();
      const tags = parseTags(this.story.currentTags || []);
      turnTags.push(...tags);

      tags
        .filter((tag) => tag.key === 'title' || tag.key === 'author')
        .forEach((tag) => globalTags.push(tag));

      if (text) {
        paragraphs.push({ text, tags });
      } else {
        tags.forEach((tag) => globalTags.push(tag));
      }
    }

    const choices = this.story.currentChoices.map((choice): ChoiceResult => {
      const tags = parseTags(choice.tags || []);
      const category = getTagValue(tags, 'action');
      const letter = getTagValue(tags, 'letter') || getTagValue(tags, 'key');
      return {
        index: choice.index,
        text: String(choice.text || '').trim(),
        tags,
        category,
        letter,
      };
    });
    const inputMode = choices.length > 0 ? 'choice' : 'end';
    const gameState: TurnResult['gameState'] = {};

    if (inputMode === 'end') {
      const errorTag = turnTags.find((tag) => tag.key === 'error');
      const scoreTag = turnTags.find((tag) => tag.key === 'score');

      if (!errorTag && !scoreTag) {
        const message = 'Ink story ended without an explicit #score ending tag.';
        const generatedErrorTag: StoryTag = { key: 'error', value: message };
        globalTags.push(generatedErrorTag);
        turnTags.push(generatedErrorTag);
      }

      const finalErrorTag = turnTags.find((tag) => tag.key === 'error');
      const finalScoreTag = turnTags.find((tag) => tag.key === 'score');
      if (finalErrorTag) {
        gameState.endState = {
          type: 'error',
          message: finalErrorTag.value || finalErrorTag.param,
        };
      } else if (finalScoreTag) {
        const numericScore = Number(finalScoreTag?.value);
        if (Number.isFinite(numericScore)) {
          gameState.score = numericScore;
        }
        gameState.endState = {
          type: 'intended',
          message: finalScoreTag.value || finalScoreTag.param,
        };
      }
    }

    return {
      turnId: this.nextTurnId++,
      paragraphs,
      choices,
      inputMode,
      globalTags: globalTags.length > 0 ? globalTags : undefined,
      gameState: Object.keys(gameState).length > 0 ? gameState : undefined,
    };
  }
}
