#!/usr/bin/env python3
"""
Ralph PRD Generator - implementare Python a skill-ului ralph-prd
Generează PRD și prd.json din descriere (heuristic) sau din final-plan.md (Opus).

Schema extinsă (W3 / smart gates + DAG):
  - tags[]            : "ui" | "db" | "vercel" | "refactor" | "docs" | "backend" | "infra"
  - dependsOn[]       : alte story IDs (DAG topological sort)
  - acceptanceCriteria: 3-5 criterii verificabile concret
  - passes/failed/blocked/retries: state pentru ralph.sh loop guard
  - failureReason     : populat când failed=true (ex: "rate_limited", "max_retries")
"""

import json
import os
import re
import subprocess
from pathlib import Path
from datetime import datetime
from typing import Optional


# Constants pentru smart gates dispatcher
VALID_TAGS = {"ui", "db", "vercel", "refactor", "docs", "backend", "infra"}
CLAUDE_BIN = os.environ.get("CLAUDE_BIN", "claude")
OPUS_TIMEOUT = int(os.environ.get("RALPH_PRD_OPUS_TIMEOUT", "600"))  # 10 min


def _normalize_story(story: dict, idx: int = 0) -> dict:
    """Asigură că un story dict are toate câmpurile schemei extinse W3."""
    sid = story.get("id") or f"US-{idx + 1:03d}"
    title = (story.get("title") or "").strip() or f"Story {sid}"
    description = (story.get("description") or "").strip() or title

    # Tags: filter la VALID_TAGS, păstrează ordine
    raw_tags = story.get("tags") or []
    tags = [t for t in raw_tags if isinstance(t, str) and t in VALID_TAGS]

    # dependsOn: lista de story IDs (string-uri)
    raw_deps = story.get("dependsOn") or []
    depends_on = [d for d in raw_deps if isinstance(d, str) and d.strip()]

    # acceptance criteria: cel puțin 1, ideal 3-5
    raw_ac = story.get("acceptanceCriteria") or []
    acceptance = [c.strip() for c in raw_ac if isinstance(c, str) and c.strip()]
    if not acceptance:
        acceptance = ["Funcționalitatea implementată conform descrierii"]

    return {
        "id": sid,
        "title": title,
        "description": description,
        "priority": int(story.get("priority") or (idx + 1) * 10),
        "acceptanceCriteria": acceptance,
        "tags": tags,
        "dependsOn": depends_on,
        "requiresBrowserCheck": bool(story.get("requiresBrowserCheck", "ui" in tags)),
        "requiresDesignReview": bool(story.get("requiresDesignReview", False)),
        "passes": bool(story.get("passes", False)),
        "failed": bool(story.get("failed", False)),
        "blocked": bool(story.get("blocked", False)),
        "retries": int(story.get("retries") or 0),
        "failureReason": story.get("failureReason") or "",
        "notes": story.get("notes") or "",
    }


def extract_stories_from_final_plan(final_plan_path: Path) -> Optional[list]:
    """Invocă Claude CLI (Opus) pe final-plan.md și extrage user stories în schema extinsă.

    Returnează listă de stories normalizate, sau None dacă invocarea eșuează.
    Backward-compat: caller-ul poate fallback la heuristic dacă None.
    """
    if not final_plan_path.exists():
        return None

    try:
        plan_content = final_plan_path.read_text(encoding="utf-8")
    except (OSError, UnicodeDecodeError):
        return None

    valid_tags_csv = ", ".join(sorted(VALID_TAGS))
    prompt = (
        f"Citește final-plan.md de mai jos și extrage user stories implementabile autonom de către un agent AI (Ralph).\n\n"
        f"Pentru fiecare story extrage:\n"
        f"- id (format US-001, US-002...)\n"
        f"- title (scurt, imperativ)\n"
        f"- description (1-2 propoziții, ce face story-ul)\n"
        f"- acceptanceCriteria[] (3-5 criterii verificabile concret — comenzi care trebuie să iasă PASS, fișiere create, comportament observabil)\n"
        f"- tags[] (subset din: {valid_tags_csv}; un story poate avea 1-3 tags)\n"
        f"- dependsOn[] (alte story IDs de care depinde — pentru DAG topological sort; goală dacă independent)\n"
        f"- priority (10, 20, 30... în ordinea din plan)\n\n"
        f"Reguli:\n"
        f"- Fiecare story IMPLEMENTABIL — nu task-uri de research sau design (alea s-au făcut deja în plan).\n"
        f"- Tags ghidează Ralph-ul să ruleze gates corecte: ui→/qa Playwright, db→schema diff, vercel→PR checks, refactor→/workflow:simplify.\n"
        f"- dependsOn pentru ordering real (US-002 are nevoie de DB-ul din US-001) — NU pentru tot ce vine după.\n"
        f"- Nu inventa stories peste plan; extrage doar ce e acolo.\n\n"
        f"Răspunde DOAR cu JSON valid (fără markdown fence, fără comentarii) în formatul:\n"
        f'{{"userStories": [{{"id":"US-001","title":"...","description":"...","acceptanceCriteria":["..."],"tags":["..."],"dependsOn":[],"priority":10}}, ...]}}\n\n'
        f"=== FINAL PLAN ===\n{plan_content}\n=== END PLAN ===\n"
    )

    cmd = [
        CLAUDE_BIN, "-p", prompt,
        "--model", "opus",
        "--output-format", "json",
    ]

    try:
        proc = subprocess.run(
            cmd,
            capture_output=True,
            text=True,
            timeout=OPUS_TIMEOUT,
        )
    except (subprocess.TimeoutExpired, FileNotFoundError) as exc:
        print(f"⚠️  Opus extraction failed: {exc}")
        return None

    if proc.returncode != 0:
        print(f"⚠️  Opus exit {proc.returncode}: {proc.stderr[:300]}")
        return None

    # Claude --output-format json wrap-uiește răspunsul în {"result": "..."}
    raw_result = proc.stdout
    try:
        wrapper = json.loads(raw_result)
        result_text = wrapper.get("result", raw_result) if isinstance(wrapper, dict) else raw_result
    except json.JSONDecodeError:
        result_text = raw_result

    # Caută primul block JSON în răspuns (defensiv contra prefix/suffix text)
    match = re.search(r'\{[\s\S]*"userStories"[\s\S]*\}', result_text)
    if not match:
        print(f"⚠️  Niciun JSON cu userStories în output Opus")
        return None

    try:
        parsed = json.loads(match.group(0))
    except json.JSONDecodeError as exc:
        print(f"⚠️  JSON parse error în output Opus: {exc}")
        return None

    raw_stories = parsed.get("userStories") or []
    if not isinstance(raw_stories, list) or not raw_stories:
        return None

    return [_normalize_story(s, i) for i, s in enumerate(raw_stories)]


def detect_project_context(project_dir: Path):
    """Detectează tech stack și mod (NEW_PROJECT vs FEATURE)"""
    context = {
        "mode": "NEW_PROJECT",
        "stack_type": None,
        "config_file": None,
        "scripts": {},
        "dependencies": []
    }
    
    # Verifică fișiere de config
    config_files = {
        "package.json": "nodejs",
        "pyproject.toml": "python",
        "requirements.txt": "python-legacy",
        "go.mod": "go",
        "Cargo.toml": "rust",
        "pom.xml": "java-maven",
        "build.gradle": "java-gradle",
        "composer.json": "php",
        "Gemfile": "ruby"
    }
    
    for filename, stack in config_files.items():
        config_path = project_dir / filename
        if config_path.exists():
            context["mode"] = "FEATURE"
            context["stack_type"] = stack
            context["config_file"] = filename
            
            # Extrage info din package.json
            if filename == "package.json":
                try:
                    with open(config_path) as f:
                        data = json.load(f)
                        context["scripts"] = data.get("scripts", {})
                        context["dependencies"] = list(data.get("dependencies", {}).keys())
                except:
                    pass
            
            break
    
    return context


def generate_prd_markdown(project_name: str, description: str, context: dict):
    """Generează PRD markdown bazat pe descriere și context"""
    
    # Parse descriere pentru a extrage info
    lines = description.strip().split('\n')
    features = []
    requirements = []
    
    for line in lines:
        line = line.strip()
        if line.startswith('Features:') or line.startswith('Feature:'):
            continue
        if line.startswith('-'):
            features.append(line[1:].strip())
        elif line and not line.endswith(':'):
            requirements.append(line)
    
    if not features:
        # Dacă nu sunt features explicit, folosim descrierea ca feature
        features = [description.split('.')[0]]
    
    # Detectează tip proiect din descriere
    desc_lower = description.lower()
    if 'cli' in desc_lower or 'command' in desc_lower:
        project_type = "CLI Tool"
    elif 'api' in desc_lower or 'backend' in desc_lower:
        project_type = "API / Backend"
    elif 'web' in desc_lower or 'app' in desc_lower:
        project_type = "Web Application"
    else:
        project_type = "Application"
    
    # Detectează stack din descriere sau context
    if 'python' in desc_lower:
        stack = "Python"
        test_framework = "pytest"
    elif 'javascript' in desc_lower or 'node' in desc_lower or 'typescript' in desc_lower:
        stack = "Node.js / TypeScript"
        test_framework = "jest"
    elif context["stack_type"] == "nodejs":
        stack = "Node.js / TypeScript"
        test_framework = "jest"
    elif context["stack_type"] and context["stack_type"].startswith("python"):
        stack = "Python"
        test_framework = "pytest"
    else:
        stack = "Python"  # Default
        test_framework = "pytest"
    
    # Template PRD
    prd = f"""# PRD: {project_name.replace('-', ' ').title()}

## 1. Introducere

{description.split('.')[0]}.

**Data:** {datetime.now().strftime('%Y-%m-%d')}
**Status:** Draft
**Mode:** {context['mode']}

## 2. Context Tehnic

"""
    
    if context["mode"] == "FEATURE":
        prd += f"""**Proiect existent detectat:**
- Stack: {context['stack_type']}
- Config: {context['config_file']}
- Scripts: {', '.join(context['scripts'].keys())}

"""
    else:
        prd += f"""**Proiect nou:**
- Tip: {project_type}
- Stack recomandat: {stack}
- Test framework: {test_framework}

"""
    
    prd += f"""## 3. Obiective

### Obiectiv Principal
{features[0] if features else description.split('.')[0]}

### Obiective Secundare
"""
    
    for i, feature in enumerate(features[1:] if len(features) > 1 else features, 1):
        prd += f"- {feature}\n"
    
    prd += f"""
### Metrici de Succes
- Toate funcționalitățile implementate conform spec
- Tests passing (coverage > 80%)
- Code quality: lint + typecheck pass

## 4. User Stories

"""
    
    # Generează user stories din features
    for i, feature in enumerate(features, 1):
        story_id = f"US-{i:03d}"
        title = feature.strip()
        
        prd += f"""### {story_id}: {title}
**Ca** utilizator
**Vreau** {title.lower()}
**Pentru că** pot folosi aplicația eficient

**Acceptance Criteria:**
- [ ] Funcționalitatea implementată conform descrierii
- [ ] Input validation în loc
- [ ] Error handling pentru cazuri edge
- [ ] Tests cu {test_framework} (coverage > 80%)
- [ ] Code quality: lint + typecheck pass

**Priority:** {i * 10}

"""
    
    # Dacă nu sunt multe features, adaugă story pentru tests
    if len(features) < 3:
        prd += f"""### US-{len(features)+1:03d}: Tests și Documentație
**Ca** developer
**Vreau** teste comprehensive și documentație
**Pentru că** asigur calitatea codului

**Acceptance Criteria:**
- [ ] Unit tests pentru toate funcțiile (coverage > 80%)
- [ ] Integration tests pentru flow-uri principale
- [ ] README cu instrucțiuni de utilizare
- [ ] Docstrings pentru funcții publice
- [ ] {test_framework} rulează fără erori

**Priority:** {(len(features)+1) * 10}

"""
    
    prd += f"""## 5. Cerințe Funcționale

"""
    
    for i, req in enumerate(requirements if requirements else features, 1):
        prd += f"{i}. [REQ-{i:03d}] {req}\n"
    
    prd += f"""
## 6. Non-Goals (Ce NU facem)

- Interfață grafică (GUI) - doar CLI/API
- Suport multiple limbaje - doar {stack}
- Deployment infrastructure - doar cod functional

## 7. Considerații Tehnice

### Stack/Tehnologii
- Limbaj: {stack}
- Testing: {test_framework}
- Linting: pylint / eslint (depinde de stack)
- Type checking: mypy / typescript

### Patterns de Urmat
- Clean code principles
- SOLID principles unde aplicabil
- Error handling consistent
- Input validation strict

### Riscuri Tehnice
- Edge cases la input validation
- Performance pentru volume mari de date (dacă aplicabil)

## 8. Considerații Security

- Input validation pentru toate datele externe
- Error messages fără info sensibilă
- Principle of least privilege

## 9. Open Questions

- [ ] Performance requirements specifice?
- [ ] Limite pe input sizes?
- [ ] Specific error handling patterns preferați?

---

**Generated by:** Echo (Ralph PRD Generator)
**Date:** {datetime.now().strftime('%Y-%m-%d %H:%M')}
"""
    
    return prd


def prd_to_stories(prd_content: str, project_name: str):
    """Extrage user stories din PRD și le convertește în format prd.json"""
    
    stories = []
    
    # Parse PRD pentru stories
    story_pattern = r'### (US-\d+): (.+?)\n\*\*Ca\*\* (.+?)\n\*\*Vreau\*\* (.+?)\n\*\*Pentru că\*\* (.+?)\n\n\*\*Acceptance Criteria:\*\*\n(.*?)\n\n\*\*Priority:\*\* (\d+)'
    
    matches = re.finditer(story_pattern, prd_content, re.DOTALL)
    
    for match in matches:
        story_id = match.group(1)
        title = match.group(2).strip()
        user_type = match.group(3).strip()
        want = match.group(4).strip()
        because = match.group(5).strip()
        criteria_text = match.group(6).strip()
        priority = int(match.group(7))
        
        # Parse acceptance criteria
        criteria = []
        for line in criteria_text.split('\n'):
            line = line.strip()
            if line.startswith('- [ ]'):
                criteria.append(line[5:].strip())
        
        # Detectează dacă necesită browser check (pentru UI)
        requires_browser = 'ui' in title.lower() or 'interface' in title.lower()
        # Heuristic tags din titlu
        tags_inferred = []
        title_lower = title.lower()
        if requires_browser or 'ui' in title_lower or 'frontend' in title_lower:
            tags_inferred.append('ui')
        if 'database' in title_lower or 'schema' in title_lower or 'migration' in title_lower:
            tags_inferred.append('db')
        if 'refactor' in title_lower or 'cleanup' in title_lower:
            tags_inferred.append('refactor')
        if 'doc' in title_lower or 'readme' in title_lower:
            tags_inferred.append('docs')

        story = _normalize_story({
            "id": story_id,
            "title": title,
            "description": f"Ca {user_type}, vreau {want} pentru că {because}",
            "priority": priority,
            "acceptanceCriteria": criteria,
            "tags": tags_inferred,
            "dependsOn": [],
            "requiresBrowserCheck": requires_browser,
        }, idx=int(story_id.split('-')[-1]) - 1 if story_id.startswith("US-") else 0)
        stories.append(story)

    # Dacă nu găsim stories (regex failed), generăm basic
    if not stories:
        stories = [_normalize_story({
            "id": "US-001",
            "title": "Implementare funcționalitate principală",
            "description": f"Implementează {project_name}",
            "priority": 10,
            "acceptanceCriteria": [
                "Funcționalitatea implementată",
                "Tests passing",
                "Lint + typecheck pass"
            ],
            "tags": [],
            "dependsOn": [],
        }, idx=0)]

    return stories


def detect_tech_stack_commands(project_dir: Path, context: dict):
    """Detectează comenzile tech stack pentru prd.json"""
    
    stack_type = context.get("stack_type", "python")
    scripts = context.get("scripts", {})
    
    # Default commands per stack
    if stack_type == "nodejs" or "package.json" in str(context.get("config_file", "")):
        commands = {
            "start": scripts.get("dev", scripts.get("start", "npm run dev")),
            "build": scripts.get("build", "npm run build"),
            "lint": scripts.get("lint", "npm run lint"),
            "typecheck": scripts.get("typecheck", "npm run typecheck"),
            "test": scripts.get("test", "npm test")
        }
        port = 3000
        stack_name = "nextjs" if "next" in str(context.get("dependencies", [])) else "nodejs"
        
    elif stack_type and stack_type.startswith("python"):
        commands = {
            "start": "python main.py",
            "build": "",
            "lint": "ruff check .",
            "typecheck": "mypy .",
            "test": "pytest"
        }
        port = 8000
        stack_name = "python"
        
    else:
        # Generic/fallback
        commands = {
            "start": "python main.py",
            "build": "",
            "lint": "echo 'No linter configured'",
            "typecheck": "echo 'No typecheck configured'",
            "test": "pytest"
        }
        port = 8000
        stack_name = "python"
    
    return {
        "type": stack_name,
        "commands": commands,
        "port": port
    }


def create_prd_and_json(
    project_name: str,
    description: str,
    workspace_dir: Path,
    final_plan_path: Optional[Path] = None,
):
    """
    Generează PRD markdown și prd.json pentru un proiect.

    Args:
        project_name: slug proiect (folder în workspace_dir)
        description: descriere scurtă (folosită ca fallback și pentru PRD markdown)
        workspace_dir: rădăcina workspace (default ~/workspace/)
        final_plan_path: opțional, calea către final-plan.md produs de planning agent (W2);
            când e furnizat, user stories sunt extrase prin Claude Opus din plan;
            când e None, păstrăm comportamentul vechi (heuristic din description).

    Returns:
        tuple: (prd_file_path, prd_json_path) sau (None, None) dacă eroare
    """
    project_dir = workspace_dir / project_name
    project_dir.mkdir(parents=True, exist_ok=True)

    # Detectează context
    context = detect_project_context(project_dir)

    print(f"📊 Context detectat:")
    print(f"  Mode: {context['mode']}")
    if context['stack_type']:
        print(f"  Stack: {context['stack_type']}")
        print(f"  Config: {context['config_file']}")
    if final_plan_path:
        print(f"  Final plan: {final_plan_path}")

    # Generează PRD markdown (mereu — folosit pentru read humans)
    prd_content = generate_prd_markdown(project_name, description, context)

    # Salvează PRD
    tasks_dir = project_dir / "tasks"
    tasks_dir.mkdir(exist_ok=True)
    prd_file = tasks_dir / f"prd-{project_name}.md"

    with open(prd_file, 'w', encoding='utf-8') as f:
        f.write(prd_content)

    print(f"✅ PRD salvat: {prd_file}")

    # Generează stories — preferă Opus din final-plan.md când disponibil
    stories = None
    if final_plan_path is not None:
        plan = Path(final_plan_path) if not isinstance(final_plan_path, Path) else final_plan_path
        print(f"🧠 Extrag stories din final-plan.md cu Opus...")
        stories = extract_stories_from_final_plan(plan)
        if stories:
            print(f"   ↳ {len(stories)} stories extrase din plan")
        else:
            print(f"   ↳ Opus extraction eșuat — fallback la heuristic")

    if not stories:
        stories = prd_to_stories(prd_content, project_name)

    tech_stack = detect_tech_stack_commands(project_dir, context)
    
    prd_json_data = {
        "projectName": project_name,
        "branchName": f"ralph/{project_name}",
        "description": description.split('\n')[0],
        "techStack": tech_stack,
        "userStories": stories
    }
    
    # Creează structura ralph
    ralph_dir = project_dir / "scripts" / "ralph"
    ralph_dir.mkdir(parents=True, exist_ok=True)
    (ralph_dir / "logs").mkdir(exist_ok=True)
    (ralph_dir / "archive").mkdir(exist_ok=True)
    (ralph_dir / "screenshots").mkdir(exist_ok=True)
    
    # Salvează prd.json
    prd_json_file = ralph_dir / "prd.json"
    with open(prd_json_file, 'w', encoding='utf-8') as f:
        json.dump(prd_json_data, f, indent=2, ensure_ascii=False)
    
    print(f"✅ prd.json salvat: {prd_json_file}")
    print(f"📋 Stories: {len(stories)}")
    for story in stories:
        print(f"  - {story['id']}: {story['title']}")
    
    # Copiază template-uri ralph
    templates_dir = Path.home() / ".claude" / "skills" / "ralph" / "templates"
    if not templates_dir.exists():
        templates_dir = Path.home() / "echo-core" / "skills" / "ralph" / "templates"
    
    if templates_dir.exists():
        # Copiază ralph.sh
        ralph_sh_src = templates_dir / "ralph.sh"
        if ralph_sh_src.exists():
            ralph_sh_dst = ralph_dir / "ralph.sh"
            with open(ralph_sh_src) as f:
                content = f.read()
            with open(ralph_sh_dst, 'w') as f:
                f.write(content)
            ralph_sh_dst.chmod(0o755)
            print(f"✅ ralph.sh copiat")
        
        # Copiază prompt.md
        prompt_src = templates_dir / "prompt.md"
        if prompt_src.exists():
            prompt_dst = ralph_dir / "prompt.md"
            with open(prompt_src) as f:
                content = f.read()
            with open(prompt_dst, 'w') as f:
                f.write(content)
            print(f"✅ prompt.md copiat")
    
    # Init progress.txt
    progress_file = ralph_dir / "progress.txt"
    with open(progress_file, 'w') as f:
        f.write(f"# Ralph Progress Log\n")
        f.write(f"Started: {datetime.now().strftime('%Y-%m-%d %H:%M')}\n")
        f.write(f"Project: {project_name}\n")
        f.write(f"---\n")
    
    print(f"✅ Structură Ralph completă în {ralph_dir}")
    
    return prd_file, prd_json_file


if __name__ == "__main__":
    import sys

    if len(sys.argv) < 3:
        print("Usage: python ralph_prd_generator.py PROJECT_NAME 'description' [final_plan_path]")
        sys.exit(1)

    project_name = sys.argv[1]
    description = sys.argv[2]
    final_plan_arg = Path(sys.argv[3]) if len(sys.argv) > 3 else None
    workspace = Path.home() / "workspace"

    print(f"🔄 Generez PRD pentru {project_name}")
    print("=" * 70)

    prd_file, prd_json = create_prd_and_json(
        project_name, description, workspace, final_plan_path=final_plan_arg
    )
    
    if prd_file and prd_json:
        print("\n" + "=" * 70)
        print("✅ PRD și prd.json generate cu succes!")
        print(f"📄 PRD: {prd_file}")
        print(f"📋 JSON: {prd_json}")
        print("\n📌 Următorii pași:")
        print(f"  1. Revizuiește PRD în {prd_file}")
        print(f"  2. Rulează Ralph: cd {prd_file.parent.parent} && ./scripts/ralph/ralph.sh 20")
        print("=" * 70)
    else:
        print("\n❌ Eroare la generare PRD")
        sys.exit(1)
