"""
Core migration engine.
Uses Anthropic Claude API to translate source code between languages.

Features:
  - Exponential backoff with jitter for rate-limit (429) and timeout errors
  - Resume support via hash-based state tracking
  - Priority-based file ordering (config/constants first)
  - Context injection (converted priority files fed to subsequent prompts)
  - Ecosystem file conversion (requirements.txt → package.json, etc.)
  - Robust output sanitization
"""
import os
import time
import random
import datetime
from typing import Callable

from anthropic import Anthropic, RateLimitError, APITimeoutError, APIStatusError

from .languages import get_syntax_name
from .sanitizer import sanitize_code_output
from .resume_state import (
    MigrationState, FileEntry, file_hash, load_state, save_state,
)
from .ecosystem import (
    get_ecosystem_mappings, sort_files_by_priority,
    get_priority_files,
)

# Default model
DEFAULT_MODEL = "claude-sonnet-4-20250514"

# Retry settings
MAX_RETRIES = 5
INITIAL_BACKOFF = 2.0   # seconds
MAX_BACKOFF = 120.0      # seconds


SYSTEM_PROMPT = """\
You are an expert code migration assistant. Your task is to convert source code \
from one programming language to another while preserving functionality, structure, \
and intent.

Rules:
1. Output ONLY the converted source code — no explanations, no markdown fences, \
   no commentary before or after the code.
2. Preserve comments (translate them if they are in a natural language different \
   from the code's target audience, otherwise keep as-is).
3. Use idiomatic patterns and best practices of the TARGET language.
4. Preserve the original file/module structure where possible.
5. If a direct equivalent does not exist, use the closest idiomatic alternative \
   and add a brief TODO comment.
6. Maintain variable/function naming conventions appropriate for the target language \
   (e.g., camelCase for Java, snake_case for Python).
7. Keep the same logical flow — do not optimize or refactor beyond what is \
   necessary for the language change.
8. Do NOT wrap output in markdown code fences (``` ... ```).
9. Do NOT add any natural language text before or after the code.
"""


ECOSYSTEM_SYSTEM_PROMPT = """\
You are an expert at converting project configuration and build files between \
programming language ecosystems. Convert the given project file to the equivalent \
in the target ecosystem.

Rules:
1. Output ONLY the converted file content — no explanations, no markdown fences.
2. Map dependencies to their equivalent packages in the target ecosystem where \
   known equivalents exist.
3. For dependencies without clear equivalents, add a comment noting they need \
   manual resolution.
4. Preserve the project structure and metadata (name, version, description, etc.).
5. Do NOT wrap output in markdown code fences.
"""


def build_user_prompt(
    source_code: str,
    source_lang: str,
    target_lang: str,
    extra_instructions: str = "",
    context_snippets: list[tuple[str, str]] | None = None,
) -> str:
    """Build the user prompt for code migration."""
    src_name = get_syntax_name(source_lang)
    tgt_name = get_syntax_name(target_lang)

    prompt = f"Convert the following {src_name} code to {tgt_name}.\n"

    if extra_instructions:
        prompt += f"\nAdditional instructions: {extra_instructions}\n"

    # Inject context from previously-converted priority files
    if context_snippets:
        prompt += "\n--- ALREADY-CONVERTED PROJECT CONTEXT (for reference) ---\n"
        for ctx_name, ctx_code in context_snippets:
            prompt += f"\n// File: {ctx_name}\n{ctx_code}\n"
        prompt += "--- END CONTEXT ---\n"

    prompt += f"\n--- SOURCE ({src_name}) ---\n{source_code}\n--- END SOURCE ---\n"
    return prompt


def build_ecosystem_prompt(
    source_content: str,
    source_filename: str,
    target_filename: str,
    source_lang: str,
    target_lang: str,
) -> str:
    """Build prompt for ecosystem file conversion."""
    src_name = get_syntax_name(source_lang)
    tgt_name = get_syntax_name(target_lang)
    return (
        f"Convert this {src_name} project file ({source_filename}) "
        f"to its {tgt_name} equivalent ({target_filename}).\n\n"
        f"--- SOURCE ({source_filename}) ---\n{source_content}\n--- END SOURCE ---\n"
    )


class Migrator:
    """Translates source code files using Claude API with retry and resume."""

    def __init__(
        self,
        api_key: str = "",
        model: str = DEFAULT_MODEL,
        max_retries: int = MAX_RETRIES,
    ):
        kwargs = {}
        if api_key:
            kwargs["api_key"] = api_key
        self.client = Anthropic(**kwargs)
        self.model = model
        self.max_retries = max_retries

    def _call_api(self, system: str, user_prompt: str) -> str:
        """Call Claude API with exponential backoff retry on 429/timeout."""
        last_error = None
        for attempt in range(self.max_retries + 1):
            try:
                response = self.client.messages.create(
                    model=self.model,
                    max_tokens=16384,
                    system=system,
                    messages=[{"role": "user", "content": user_prompt}],
                )
                return response.content[0].text
            except (RateLimitError, APITimeoutError) as e:
                last_error = e
                if attempt == self.max_retries:
                    break
                backoff = min(INITIAL_BACKOFF * (2 ** attempt), MAX_BACKOFF)
                jitter = random.uniform(0, backoff * 0.3)
                wait = backoff + jitter
                if self._on_retry:
                    self._on_retry(attempt + 1, wait, str(e))
                time.sleep(wait)
            except APIStatusError as e:
                if e.status_code == 529:  # Overloaded
                    last_error = e
                    if attempt == self.max_retries:
                        break
                    backoff = min(INITIAL_BACKOFF * (2 ** attempt), MAX_BACKOFF)
                    jitter = random.uniform(0, backoff * 0.3)
                    wait = backoff + jitter
                    if self._on_retry:
                        self._on_retry(attempt + 1, wait, str(e))
                    time.sleep(wait)
                else:
                    raise
        raise last_error  # type: ignore[misc]

    _on_retry: Callable[[int, float, str], None] | None = None

    def migrate_code(
        self,
        source_code: str,
        source_lang: str,
        target_lang: str,
        extra_instructions: str = "",
        context_snippets: list[tuple[str, str]] | None = None,
    ) -> str:
        """Migrate a single piece of source code. Returns sanitized converted code."""
        user_prompt = build_user_prompt(
            source_code, source_lang, target_lang,
            extra_instructions, context_snippets,
        )
        raw = self._call_api(SYSTEM_PROMPT, user_prompt)
        return sanitize_code_output(raw, get_syntax_name(target_lang))

    def migrate_ecosystem_file(
        self,
        source_content: str,
        source_filename: str,
        target_filename: str,
        source_lang: str,
        target_lang: str,
    ) -> str:
        """Migrate an ecosystem file (e.g. requirements.txt → package.json)."""
        user_prompt = build_ecosystem_prompt(
            source_content, source_filename, target_filename,
            source_lang, target_lang,
        )
        raw = self._call_api(ECOSYSTEM_SYSTEM_PROMPT, user_prompt)
        return sanitize_code_output(raw)

    def migrate_folder(
        self,
        input_dir: str,
        output_dir: str,
        source_lang: str,
        target_lang: str,
        source_extensions: list[str],
        target_extension: str,
        extra_instructions: str = "",
        on_progress: Callable[[str, int, int], None] | None = None,
        on_log: Callable[[str, str], None] | None = None,
        cancel_check: Callable[[], bool] | None = None,
        encoding: str = "utf-8",
        enable_resume: bool = True,
        enable_context: bool = True,
        enable_ecosystem: bool = True,
    ) -> dict:
        """
        Migrate all matching files in input_dir to output_dir.

        Features:
          - Resume: skips files already converted (hash-checked)
          - Context: converts priority files first, feeds them as context
          - Ecosystem: converts project files (requirements.txt, etc.)
          - Retry: exponential backoff on rate-limit / timeout

        Returns dict with counts and details.
        """

        def _log(msg: str, tag: str = "info"):
            if on_log:
                on_log(msg, tag)

        # Wire up retry logging
        def _on_retry(attempt: int, wait: float, err: str):
            _log(f"  Retry {attempt}/{self.max_retries} in {wait:.1f}s: {err}", "warning")
        self._on_retry = _on_retry

        # --- Load resume state ---
        state = load_state(output_dir) if enable_resume else MigrationState()
        state.source_lang = source_lang
        state.target_lang = target_lang

        # --- Collect source files ---
        source_files = []
        ext_lower_set = {e.lower() for e in source_extensions}
        for root, _dirs, files in os.walk(input_dir):
            for f in files:
                ext = os.path.splitext(f)[1].lower()
                if ext in ext_lower_set:
                    source_files.append(os.path.join(root, f))

        # --- Sort by priority (config/constants first) ---
        source_files = sort_files_by_priority(source_files)

        # --- Identify priority files for context injection ---
        priority_files = get_priority_files(source_files) if enable_context else []
        context_snippets: list[tuple[str, str]] = []

        # --- Ecosystem file detection ---
        eco_mappings = get_ecosystem_mappings(source_lang, target_lang) if enable_ecosystem else {}
        eco_files: list[tuple[str, str, str]] = []  # (source_path, source_name, target_name)
        if eco_mappings:
            for root, _dirs, files in os.walk(input_dir):
                for f in files:
                    if f in eco_mappings:
                        eco_files.append((
                            os.path.join(root, f), f, eco_mappings[f]
                        ))

        total = len(source_files) + len(eco_files)
        results = {"success": 0, "errors": 0, "skipped": 0, "resumed": 0, "details": []}
        file_idx = 0

        # --- Phase 1: Ecosystem files ---
        for eco_path, eco_src_name, eco_tgt_name in eco_files:
            if cancel_check and cancel_check():
                results["skipped"] += total - file_idx
                results["details"].append(("", "CANCELLED", ""))
                break

            file_idx += 1
            if on_progress:
                on_progress(eco_src_name, file_idx, total)

            rel_path = os.path.relpath(eco_path, input_dir)

            try:
                # Check resume state
                src_hash = file_hash(eco_path, encoding)
                if enable_resume and state.is_done(rel_path, src_hash):
                    results["resumed"] += 1
                    _log(f"  [RESUME] {eco_src_name} (already converted)", "info")
                    continue

                with open(eco_path, "r", encoding=encoding, errors="replace") as f:
                    content = f.read()

                if not content.strip():
                    results["skipped"] += 1
                    continue

                _log(f"  Ecosystem: {eco_src_name} → {eco_tgt_name}", "info")

                converted = self.migrate_ecosystem_file(
                    content, eco_src_name, eco_tgt_name,
                    source_lang, target_lang,
                )

                out_dir = os.path.join(
                    output_dir,
                    os.path.relpath(os.path.dirname(eco_path), input_dir),
                )
                os.makedirs(out_dir, exist_ok=True)
                out_path = os.path.join(out_dir, eco_tgt_name)

                with open(out_path, "w", encoding="utf-8", newline="\n") as f:
                    f.write(converted)

                results["success"] += 1
                results["details"].append((eco_src_name, "OK", out_path))

                # Save state
                now = datetime.datetime.now().isoformat()
                state.mark_done(rel_path, FileEntry(
                    source_hash=src_hash, source_path=eco_path,
                    output_path=out_path, status="ok", timestamp=now,
                ))
                if enable_resume:
                    save_state(output_dir, state)

            except Exception as e:
                results["errors"] += 1
                results["details"].append((eco_src_name, "ERROR", str(e)))
                now = datetime.datetime.now().isoformat()
                state.mark_done(rel_path, FileEntry(
                    source_hash="", source_path=eco_path,
                    output_path="", status="error", timestamp=now,
                ))
                if enable_resume:
                    save_state(output_dir, state)

        # --- Phase 2: Source code files ---
        for filepath in source_files:
            if cancel_check and cancel_check():
                results["skipped"] += total - file_idx
                results["details"].append(("", "CANCELLED", ""))
                break

            file_idx += 1
            filename = os.path.basename(filepath)
            rel_path = os.path.relpath(filepath, input_dir)

            if on_progress:
                on_progress(filename, file_idx, total)

            try:
                src_hash = file_hash(filepath, encoding)

                # Check resume state
                if enable_resume and state.is_done(rel_path, src_hash):
                    results["resumed"] += 1
                    _log(f"  [RESUME] {filename} (already converted)", "info")
                    # If this was a priority file, load its converted output for context
                    if enable_context and filepath in priority_files:
                        entry = state.get_entry(rel_path)
                        if entry and entry.get("output_path"):
                            try:
                                with open(entry["output_path"], "r", encoding="utf-8") as cf:
                                    context_snippets.append((filename, cf.read()))
                            except OSError:
                                pass
                    continue

                with open(filepath, "r", encoding=encoding, errors="replace") as f:
                    source_code = f.read()

                if not source_code.strip():
                    results["skipped"] += 1
                    _log(f"  [SKIP] {filename} (empty file)", "warning")
                    continue

                # Use context only for non-priority files
                ctx = context_snippets if (enable_context and filepath not in priority_files) else None

                converted = self.migrate_code(
                    source_code, source_lang, target_lang,
                    extra_instructions, ctx,
                )

                # Build output path
                base_name = os.path.splitext(rel_path)[0]
                out_path = os.path.join(output_dir, base_name + target_extension)
                os.makedirs(os.path.dirname(out_path), exist_ok=True)

                with open(out_path, "w", encoding="utf-8", newline="\n") as f:
                    f.write(converted)

                results["success"] += 1
                results["details"].append((filename, "OK", out_path))

                # If this is a priority file, store its converted output for context
                if enable_context and filepath in priority_files:
                    context_snippets.append((filename, converted))

                # Save resume state
                now = datetime.datetime.now().isoformat()
                state.mark_done(rel_path, FileEntry(
                    source_hash=src_hash, source_path=filepath,
                    output_path=out_path, status="ok", timestamp=now,
                ))
                if enable_resume:
                    save_state(output_dir, state)

            except Exception as e:
                results["errors"] += 1
                results["details"].append((filename, "ERROR", str(e)))
                now = datetime.datetime.now().isoformat()
                state.mark_done(rel_path, FileEntry(
                    source_hash="", source_path=filepath,
                    output_path="", status="error", timestamp=now,
                ))
                if enable_resume:
                    save_state(output_dir, state)

        # Final save
        if enable_resume:
            save_state(output_dir, state)

        self._on_retry = None
        return results
