#!/usr/bin/env python3
"""
JanexAI CLI - Scan repositories for security and quality issues.

Usage:
    janex scan https://github.com/user/repo
    janex scan owner/repo
"""

import os
import re
import sys
import json
import time
from pathlib import Path

import typer
import requests
from rich.console import Console
from rich.progress import Progress, SpinnerColumn, TextColumn
from rich.panel import Panel
from rich.table import Table
from rich import print as rprint

from . import __version__

# API Configuration
DEFAULT_API_URL = "https://api.janexai.dev"
CONFIG_DIR = Path.home() / ".janex"
CONFIG_FILE = CONFIG_DIR / "config.json"

app = typer.Typer(
    name="janex",
    help="JanexAI - AI-powered code security and quality analysis",
    add_completion=False,
)
console = Console()


def get_config() -> dict:
    """Load config from ~/.janex/config.json"""
    if CONFIG_FILE.exists():
        try:
            return json.loads(CONFIG_FILE.read_text())
        except Exception:
            return {}
    return {}


def save_config(config: dict):
    """Save config to ~/.janex/config.json with secure permissions."""
    CONFIG_DIR.mkdir(parents=True, exist_ok=True, mode=0o700)
    CONFIG_FILE.write_text(json.dumps(config, indent=2))
    # Restrict file permissions to owner only (Unix)
    try:
        CONFIG_FILE.chmod(0o600)
    except Exception:
        pass  # Windows doesn't support chmod the same way


def get_api_url() -> str:
    """Get API URL from config or environment."""
    return os.getenv("JANEX_API_URL") or get_config().get("api_url") or DEFAULT_API_URL


def get_api_token() -> str | None:
    """Get API token from config or environment."""
    return os.getenv("JANEX_API_TOKEN") or get_config().get("api_token")


def parse_repo_url(repo_input: str) -> tuple[str, str]:
    """Parse GitHub URL or owner/repo format into (owner, repo)."""
    # Handle full GitHub URLs
    github_patterns = [
        r'https?://github\.com/([^/]+)/([^/]+?)(?:\.git)?/?$',
        r'git@github\.com:([^/]+)/([^/]+?)(?:\.git)?$',
    ]
    
    for pattern in github_patterns:
        match = re.match(pattern, repo_input)
        if match:
            return match.group(1), match.group(2)
    
    # Handle owner/repo format
    if '/' in repo_input and not repo_input.startswith('http'):
        parts = repo_input.split('/')
        if len(parts) == 2:
            return parts[0], parts[1]
    
    raise typer.BadParameter(
        f"Invalid repository format: {repo_input}\n"
        "Use: https://github.com/owner/repo or owner/repo"
    )


def api_request(
    method: str,
    endpoint: str,
    data: dict = None,
    timeout: int = 300,
) -> requests.Response:
    """Make an authenticated API request."""
    api_url = get_api_url()
    token = get_api_token()
    
    # Security: Enforce HTTPS for non-localhost URLs
    if not api_url.startswith("https://") and "localhost" not in api_url and "127.0.0.1" not in api_url:
        console.print("[red]Error:[/red] API URL must use HTTPS for security.")
        console.print(f"Current URL: {api_url}")
        raise typer.Exit(1)
    
    headers = {"Content-Type": "application/json"}
    if token:
        headers["Authorization"] = f"Bearer {token}"
    
    url = f"{api_url}/api{endpoint}"
    
    try:
        if method == "GET":
            response = requests.get(url, headers=headers, timeout=timeout)
        elif method == "POST":
            response = requests.post(url, headers=headers, json=data, timeout=timeout)
        else:
            raise ValueError(f"Unsupported method: {method}")
        
        return response
    except requests.exceptions.SSLError as e:
        console.print("[red]SSL Error:[/red] Could not verify server certificate.")
        console.print("This could indicate a man-in-the-middle attack.")
        raise typer.Exit(1)
    except requests.exceptions.ConnectionError:
        console.print(f"[red]Error:[/red] Could not connect to {api_url}")
        console.print("Check your internet connection or API URL.")
        raise typer.Exit(1)
    except requests.exceptions.Timeout:
        console.print("[red]Error:[/red] Request timed out")
        raise typer.Exit(1)


@app.command()
def login(
    token: str = typer.Option(None, "--token", "-t", help="API token (prefer interactive prompt for security)"),
):
    """
    Login with your JanexAI API token.
    
    Get your token at: https://janex.ai/settings
    
    For better security, run without --token flag to enter interactively,
    or use JANEX_API_TOKEN environment variable.
    """
    if token:
        # Warn about command-line token visibility
        console.print("[yellow]Warning:[/yellow] Token passed via command line may be visible in shell history.")
        console.print("Consider using interactive login or JANEX_API_TOKEN env variable instead.\n")
    else:
        token = typer.prompt("Enter your API token", hide_input=True)
    
    if not token or len(token) < 10:
        console.print("[red]Error:[/red] Invalid token format")
        raise typer.Exit(1)
    
    # Save config with secure permissions
    config = get_config()
    config["api_token"] = token
    save_config(config)
    
    # Test the token by calling authenticated endpoint
    try:
        response = api_request("GET", "/me/")
        if response.status_code == 200:
            console.print("[green]✓[/green] Successfully logged in!")
            console.print(f"Config saved to: {CONFIG_FILE}")
        elif response.status_code == 401:
            console.print("[red]Error:[/red] Invalid API token")
            # Remove invalid token
            config.pop("api_token", None)
            save_config(config)
            raise typer.Exit(1)
        else:
            console.print("[yellow]Token saved, but could not verify.[/yellow]")
            console.print(f"Server returned: {response.status_code}")
    except Exception as e:
        console.print(f"[yellow]Token saved, but could not verify: {e}[/yellow]")


@app.command()
def logout():
    """Logout and remove stored credentials."""
    config = get_config()
    if "api_token" in config:
        del config["api_token"]
        save_config(config)
    console.print("[green]✓[/green] Logged out")


@app.command()
def config(
    api_url: str = typer.Option(None, "--api-url", help="Set custom API URL"),
    show: bool = typer.Option(False, "--show", help="Show current config"),
):
    """Configure JanexAI CLI settings."""
    cfg = get_config()
    
    if show:
        console.print(Panel(
            f"API URL: {get_api_url()}\n"
            f"Token: {'********' if get_api_token() else '(not set)'}",
            title="Configuration",
        ))
        return
    
    if api_url:
        cfg["api_url"] = api_url
        save_config(cfg)
        console.print(f"[green]✓[/green] API URL set to: {api_url}")


@app.command()
def scan(
    repo: str = typer.Argument(..., help="GitHub repo URL or owner/repo"),
    output: str = typer.Option(None, "--output", "-o", help="Output file (JSON)"),
    no_claude: bool = typer.Option(False, "--no-claude", help="Skip Claude analysis"),
    watch: bool = typer.Option(False, "--watch", "-w", help="Watch and wait for results"),
    verbose: bool = typer.Option(False, "--verbose", "-v", help="Verbose output"),
    force: bool = typer.Option(False, "--force", "-f", help="Force re-ingest (delete cached data)"),
):
    """
    Scan a GitHub repository for security and quality issues.
    
    Examples:
        janex scan https://github.com/user/repo
        janex scan user/repo
        janex scan user/repo -o report.json
    """
    try:
        owner, repo_name = parse_repo_url(repo)
    except typer.BadParameter as e:
        console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(1)
    
    full_name = f"{owner}/{repo_name}"
    
    console.print(Panel(
        f"[bold]Scanning repository:[/bold] {full_name}",
        title="JanexAI",
        border_style="blue"
    ))
    
    try:
        with Progress(
            SpinnerColumn(),
            TextColumn("[progress.description]{task.description}"),
            console=console,
        ) as progress:
            
            # Step 0: If force, delete existing repo first
            if force:
                task = progress.add_task("Deleting cached data...", total=None)
                # Try to find and delete existing repo
                try:
                    check_response = api_request("GET", f"/repositories/{owner}/{repo_name}/", timeout=30)
                    if check_response.status_code == 200:
                        existing = check_response.json()
                        existing_id = existing.get("id")
                        if existing_id:
                            del_response = api_request("POST", f"/repositories/{existing_id}/delete/", timeout=30)
                            if del_response.status_code == 200:
                                progress.update(task, description="[green]✓[/green] Cached data deleted")
                            else:
                                progress.update(task, description="[yellow]![/yellow] Could not delete (continuing anyway)")
                except Exception:
                    pass  # Ignore errors, just proceed with ingestion
            
            # Step 1: Ingest repository
            task = progress.add_task("Ingesting repository...", total=None)
            
            response = api_request("POST", "/repositories/ingest/", {
                "owner": owner,
                "repo": repo_name,
                "force": force,  # Tell backend to force re-ingest
            }, timeout=120)
            
            if response.status_code == 401:
                console.print("\n[red]Error:[/red] Authentication required.")
                console.print("Run [bold]janex login[/bold] to authenticate.")
                raise typer.Exit(1)
            
            if response.status_code not in (200, 201):
                error = response.json().get("error", response.text)
                console.print(f"\n[red]Error:[/red] {error}")
                raise typer.Exit(1)
            
            ingest_data = response.json()
            repo_id = ingest_data.get("id") or ingest_data.get("repository_id")
            already_exists = ingest_data.get("already_exists", False)
            
            if not repo_id:
                console.print("[red]Error:[/red] No repository ID returned")
                if verbose:
                    console.print(f"Response: {ingest_data}")
                raise typer.Exit(1)
            
            progress.update(task, description=f"[green]✓[/green] Repository ingested ({ingest_data.get('files_count', 0)} files)")
            
            if verbose:
                console.print(f"  Commit: {ingest_data.get('commit_sha', 'unknown')[:8]}")
            
            report = None
            
            # If repo already exists (not forced), check for existing reports first
            if already_exists and not force:
                progress.update(task, description="Checking for existing reports...")
                reports_response = api_request("GET", f"/repositories/{repo_id}/reports/", timeout=30)
                
                if reports_response.status_code == 200:
                    reports_data = reports_response.json()
                    reports_list = reports_data.get("reports", [])
                    
                    if reports_list:
                        # Get the latest report
                        latest_report_id = reports_list[0].get("id")
                        if latest_report_id:
                            progress.update(task, description="Fetching existing report...")
                            report_response = api_request("GET", f"/reports/{latest_report_id}/", timeout=30)
                            
                            if report_response.status_code == 200:
                                report = report_response.json()
                                # Normalize: get_report returns 'analysis_result', analyze returns 'analysis'
                                if 'analysis_result' in report and 'analysis' not in report:
                                    report['analysis'] = report['analysis_result']
                                progress.update(task, description="[green]✓[/green] Using existing report")
                                if verbose:
                                    console.print("  [dim]Using cached report. Use -f to force re-analysis.[/dim]")
            
            # If no existing report, run analysis
            if report is None:
                progress.update(task, description="Running security & quality analysis...")
                
                analyze_response = api_request("POST", f"/repositories/{repo_id}/analyze/", {
                    "use_claude": not no_claude,
                }, timeout=600)  # 10 min timeout for analysis
                
                if analyze_response.status_code != 200:
                    try:
                        error = analyze_response.json().get("error", analyze_response.text)
                    except Exception:
                        error = analyze_response.text or f"Server returned {analyze_response.status_code}"
                    console.print(f"\n[red]Analysis Error:[/red] {error}")
                    raise typer.Exit(1)
                
                try:
                    report = analyze_response.json()
                except Exception:
                    console.print("\n[red]Error:[/red] Server returned invalid response (connection may have dropped)")
                    console.print("The server might have restarted during analysis. Try again.")
                    raise typer.Exit(1)
            
            progress.update(task, description="[green]✓[/green] Analysis complete!")
        
        # Output results
        if output:
            output_path = Path(output).resolve()
            # Basic safety check - don't write to system directories
            unsafe_prefixes = ['/etc', '/usr', '/bin', '/sbin', '/var', '/System', 'C:\\Windows']
            if any(str(output_path).startswith(prefix) for prefix in unsafe_prefixes):
                console.print(f"[red]Error:[/red] Cannot write to system directory: {output_path}")
                raise typer.Exit(1)
            
            with open(output_path, 'w') as f:
                json.dump(report, f, indent=2, default=str)
            console.print(f"\n[green]Report saved to:[/green] {output_path}")
        
        # Display summary or full report
        if verbose:
            display_full_report(report)
        else:
            display_summary(report)
            console.print("[dim]Use -v for detailed report, or -o report.json to save full results[/dim]")
        
    except requests.exceptions.RequestException as e:
        console.print(f"[red]Request Error:[/red] {e}")
        raise typer.Exit(1)


def display_full_report(report: dict):
    """Display detailed scan report."""
    console.print()
    
    # The actual report data is nested under 'analysis'
    analysis = report.get("analysis", {})
    if not analysis:
        console.print("[yellow]No detailed analysis data found[/yellow]")
        display_summary(report)
        return
    
    # Architecture Summary
    arch_summary = (
        analysis.get("arch_summary") or 
        analysis.get("architecture_summary") or
        analysis.get("summary") or
        analysis.get("executive_summary")
    )
    if arch_summary:
        console.print(Panel(
            arch_summary,
            title="[bold blue]Architecture Summary[/bold blue]",
            border_style="blue"
        ))
        console.print()
    
    # Trust Score from analysis
    trust_score = analysis.get("trust_score")
    scores = analysis.get("scores", {})
    if trust_score or scores:
        console.print(f"[bold]Trust Score:[/bold] {trust_score or 'N/A'}/100")
        if scores:
            console.print(f"  Security: {scores.get('security', 'N/A')}/100")
            console.print(f"  Quality: {scores.get('quality', 'N/A')}/100")
            console.print(f"  Architecture: {scores.get('architecture', 'N/A')}/100")
        console.print()
    
    # Structural Findings (from arch agent)
    structural_findings = analysis.get("structural_findings") or analysis.get("findings", [])
    if structural_findings:
        console.print("[bold red]Findings[/bold red]")
        console.print("=" * 60)
        for i, finding in enumerate(structural_findings[:20], 1):
            if isinstance(finding, dict):
                severity = str(finding.get("severity", "medium")).upper()
                title = finding.get("title") or finding.get("finding_type") or finding.get("type", "Issue")
                desc = finding.get("description") or finding.get("summary", "")
                location = finding.get("location") or finding.get("file", "")
                
                color = "red" if severity == "HIGH" else "yellow" if severity == "MEDIUM" else "green"
                console.print(f"\n[{color}][{severity}][/{color}] {title}")
                if location:
                    console.print(f"  [dim]Location:[/dim] {location}")
                if desc:
                    console.print(f"  {desc[:400]}")
            else:
                console.print(f"\n• {finding}")
        console.print()
    
    # Fix Prompts
    fix_prompts = analysis.get("fix_prompts") or analysis.get("fixes", [])
    if fix_prompts:
        console.print("[bold green]Suggested Fixes (Cursor-ready)[/bold green]")
        console.print("=" * 60)
        for i, fix in enumerate(fix_prompts[:10], 1):
            if isinstance(fix, dict):
                title = fix.get("title") or fix.get("finding_type") or f"Fix {i}"
                prompt = fix.get("prompt") or fix.get("fix_prompt") or fix.get("description", "")
                console.print(f"\n[green]{i}. {title}[/green]")
                if prompt:
                    console.print(f"  {prompt[:600]}")
            else:
                console.print(f"\n{i}. {fix}")
        console.print()
    
    # Architecture Map
    arch_map = analysis.get("architecture_map", {})
    if arch_map:
        console.print("[bold blue]Architecture Map[/bold blue]")
        console.print("=" * 60)
        
        entry_points = arch_map.get("entry_points", [])
        if entry_points:
            console.print("\n[bold]Entry Points:[/bold]")
            for ep in entry_points[:10]:
                if isinstance(ep, dict):
                    console.print(f"  • {ep.get('file', '')} - {ep.get('type', '')}")
                else:
                    console.print(f"  • {ep}")
        
        key_modules = arch_map.get("key_modules", [])
        if key_modules:
            console.print("\n[bold]Key Modules:[/bold]")
            for mod in key_modules[:10]:
                if isinstance(mod, dict):
                    console.print(f"  • {mod.get('name', mod.get('file', ''))}")
                else:
                    console.print(f"  • {mod}")
        
        console.print()
    
    # Raw response structure hint
    console.print("[dim]Use -o report.json to save the full detailed report[/dim]")
    console.print()


def display_summary(report: dict):
    """Display a summary of the scan results."""
    console.print()
    
    # Get scores from analysis.scores or top-level
    analysis = report.get("analysis", {})
    scores = analysis.get("scores", {})
    
    security_score = scores.get("security") or report.get("security_score") or 0
    quality_score = scores.get("quality") or report.get("quality_score") or 0
    
    # Score colors
    def score_color(score):
        if score >= 80:
            return "green"
        elif score >= 60:
            return "yellow"
        return "red"
    
    scores_table = Table(title="Scores", show_header=True)
    scores_table.add_column("Category", style="bold")
    scores_table.add_column("Score", justify="right")
    
    scores_table.add_row(
        "Security",
        f"[{score_color(security_score)}]{security_score}/100[/{score_color(security_score)}]"
    )
    scores_table.add_row(
        "Quality", 
        f"[{score_color(quality_score)}]{quality_score}/100[/{score_color(quality_score)}]"
    )
    
    console.print(scores_table)
    console.print()
    
    # Security findings
    security = report.get("security_summary", {})
    by_severity = security.get("by_severity", {})
    high_count = by_severity.get("high", 0) or security.get("high_severity_count", 0)
    medium_count = by_severity.get("medium", 0) or security.get("medium_severity_count", 0)
    low_count = by_severity.get("low", 0) or security.get("low_severity_count", 0)
    total_security = security.get("total_findings", 0)
    
    if high_count or medium_count or low_count or total_security:
        security_table = Table(title="Security Findings", show_header=True)
        security_table.add_column("Severity", style="bold")
        security_table.add_column("Count", justify="right")
        
        if high_count > 0:
            security_table.add_row("[red]High[/red]", str(high_count))
        if medium_count > 0:
            security_table.add_row("[yellow]Medium[/yellow]", str(medium_count))
        if low_count > 0:
            security_table.add_row("[green]Low[/green]", str(low_count))
        if total_security and not (high_count or medium_count or low_count):
            security_table.add_row("Total", str(total_security))
        
        console.print(security_table)
    else:
        console.print("[green]✓ No security issues found[/green]")
    
    console.print()
    
    # Quality findings
    quality = report.get("quality_summary", {})
    quality_count = quality.get("total_issues", 0) or quality.get("total_findings", 0)
    
    if quality_count:
        console.print(f"[yellow]Quality issues:[/yellow] {quality_count}")
    else:
        console.print("[green]✓ No quality issues found[/green]")
    
    console.print()
    
    # Architecture summary
    arch_summary = report.get("arch_summary")
    if arch_summary:
        console.print(Panel(
            arch_summary[:600] + "..." if len(arch_summary) > 600 else arch_summary,
            title="Architecture Summary",
            border_style="blue"
        ))


@app.command()
def version():
    """Show version information."""
    console.print(f"JanexAI CLI v{__version__}")


def main():
    """Entry point for the CLI."""
    app()


if __name__ == "__main__":
    main()
