"""
Repository fuer Test Registry Datenbank-Operationen.

Abstrahiert alle DB-Zugriffe fuer:
- Test-Runs speichern und abrufen
- Test-Ergebnisse verwalten
- Backlog-Items verwalten
- Service-Statistiken aktualisieren
"""
from datetime import datetime
from typing import List, Optional, Dict, Any
from sqlalchemy.orm import Session
from sqlalchemy import func, desc

from .db_models import (
    TestRunDB,
    TestResultDB,
    FailedTestBacklogDB,
    TestFixHistoryDB,
    TestServiceStatsDB
)


class TestRepository:
    """Repository fuer Test-bezogene Datenbank-Operationen."""

    def __init__(self, db: Session):
        self.db = db

    # ========================================
    # Test Runs
    # ========================================

    def create_run(
        self,
        run_id: str,
        service: str,
        framework: str,
        triggered_by: str = "manual",
        git_commit: Optional[str] = None,
        git_branch: Optional[str] = None
    ) -> TestRunDB:
        """Erstellt einen neuen Test-Run."""
        run = TestRunDB(
            run_id=run_id,
            service=service,
            framework=framework,
            started_at=datetime.utcnow(),
            status="running",
            triggered_by=triggered_by,
            git_commit=git_commit,
            git_branch=git_branch
        )
        self.db.add(run)
        self.db.commit()
        self.db.refresh(run)
        return run

    def complete_run(
        self,
        run_id: str,
        status: str,
        total_tests: int,
        passed_tests: int,
        failed_tests: int,
        skipped_tests: int = 0,
        duration_seconds: float = 0,
        output: Optional[str] = None
    ) -> Optional[TestRunDB]:
        """Markiert einen Run als abgeschlossen und aktualisiert Statistiken."""
        run = self.db.query(TestRunDB).filter(TestRunDB.run_id == run_id).first()
        if run:
            run.completed_at = datetime.utcnow()
            run.status = status
            run.total_tests = total_tests
            run.passed_tests = passed_tests
            run.failed_tests = failed_tests
            run.skipped_tests = skipped_tests
            run.duration_seconds = duration_seconds
            run.output = output[:10000] if output else None  # Truncate output
            self.db.commit()
            self.db.refresh(run)

            # Aktualisiere Service-Statistiken
            self._update_service_stats(run)

        return run

    def get_run(self, run_id: str) -> Optional[TestRunDB]:
        """Holt einen Run anhand der ID."""
        return self.db.query(TestRunDB).filter(TestRunDB.run_id == run_id).first()

    def get_runs(
        self,
        service: Optional[str] = None,
        limit: int = 50,
        offset: int = 0
    ) -> List[TestRunDB]:
        """Holt Test-Runs mit optionalem Service-Filter."""
        query = self.db.query(TestRunDB)
        if service:
            query = query.filter(TestRunDB.service == service)
        return query.order_by(desc(TestRunDB.started_at)).offset(offset).limit(limit).all()

    def get_runs_count(self, service: Optional[str] = None) -> int:
        """Zaehlt Test-Runs."""
        query = self.db.query(func.count(TestRunDB.id))
        if service:
            query = query.filter(TestRunDB.service == service)
        return query.scalar() or 0

    # ========================================
    # Test Results
    # ========================================

    def add_results(self, run_id: str, results: List[Dict[str, Any]]) -> int:
        """Fuegt mehrere Test-Ergebnisse hinzu."""
        count = 0
        for result in results:
            db_result = TestResultDB(
                run_id=run_id,
                test_name=result.get("name") or result.get("test_name", "unknown"),
                test_file=result.get("file_path") or result.get("test_file"),
                line_number=result.get("line_number"),
                status=result.get("status", "unknown"),
                duration_ms=result.get("duration_ms"),
                error_message=result.get("error_message"),
                error_type=result.get("error_type"),
                output=result.get("output")
            )
            self.db.add(db_result)
            count += 1

            # Bei fehlgeschlagenen Tests: Backlog aktualisieren
            if result.get("status") in ["failed", "error"]:
                self._update_backlog(
                    run_id=run_id,
                    test_name=result.get("name") or result.get("test_name", "unknown"),
                    test_file=result.get("file_path") or result.get("test_file"),
                    error_message=result.get("error_message"),
                    error_type=result.get("error_type"),
                    suggestion=result.get("suggestion")
                )

        self.db.commit()
        return count

    def get_results(self, run_id: str) -> List[TestResultDB]:
        """Holt alle Ergebnisse eines Runs."""
        return self.db.query(TestResultDB).filter(TestResultDB.run_id == run_id).all()

    def get_failed_results(self, run_id: str) -> List[TestResultDB]:
        """Holt nur fehlgeschlagene Ergebnisse eines Runs."""
        return self.db.query(TestResultDB).filter(
            TestResultDB.run_id == run_id,
            TestResultDB.status.in_(["failed", "error"])
        ).all()

    # ========================================
    # Backlog
    # ========================================

    def _update_backlog(
        self,
        run_id: str,
        test_name: str,
        test_file: Optional[str],
        error_message: Optional[str],
        error_type: Optional[str],
        suggestion: Optional[str] = None
    ):
        """Aktualisiert oder erstellt einen Backlog-Eintrag fuer einen fehlgeschlagenen Test."""
        # Hole den Run um Service und Framework zu bekommen
        run = self.db.query(TestRunDB).filter(TestRunDB.run_id == run_id).first()
        if not run:
            return

        # Suche nach existierendem Backlog-Eintrag
        backlog = self.db.query(FailedTestBacklogDB).filter(
            FailedTestBacklogDB.test_name == test_name,
            FailedTestBacklogDB.service == run.service
        ).first()

        now = datetime.utcnow()

        if backlog:
            # Existiert bereits - aktualisiere
            backlog.last_failed_at = now
            backlog.failure_count += 1
            backlog.error_message = error_message or backlog.error_message
            backlog.error_type = error_type or backlog.error_type
            if suggestion:
                backlog.fix_suggestion = suggestion
            # Reset status wenn es wieder fehlschlaegt
            if backlog.status == "fixed":
                backlog.status = "open"
                backlog.notes = f"Erneut fehlgeschlagen nach Fix am {now.isoformat()}"
        else:
            # Neu erstellen
            backlog = FailedTestBacklogDB(
                test_name=test_name,
                test_file=test_file,
                service=run.service,
                framework=run.framework,
                error_message=error_message,
                error_type=error_type,
                first_failed_at=now,
                last_failed_at=now,
                failure_count=1,
                status="open",
                priority=self._calculate_priority(error_type),
                fix_suggestion=suggestion
            )
            self.db.add(backlog)

    def _calculate_priority(self, error_type: Optional[str]) -> str:
        """Berechnet Prioritaet basierend auf Fehlertyp."""
        high_priority = ["nil_pointer", "panic", "security", "critical"]
        medium_priority = ["assertion", "type_error", "value_error"]

        if error_type:
            if any(p in error_type.lower() for p in high_priority):
                return "high"
            if any(p in error_type.lower() for p in medium_priority):
                return "medium"
        return "medium"

    def get_backlog(
        self,
        status: Optional[str] = None,
        service: Optional[str] = None,
        priority: Optional[str] = None,
        limit: int = 100,
        offset: int = 0
    ) -> List[FailedTestBacklogDB]:
        """Holt Backlog-Eintraege mit optionalen Filtern."""
        query = self.db.query(FailedTestBacklogDB)

        if status:
            query = query.filter(FailedTestBacklogDB.status == status)
        if service:
            query = query.filter(FailedTestBacklogDB.service == service)
        if priority:
            query = query.filter(FailedTestBacklogDB.priority == priority)

        return query.order_by(
            desc(FailedTestBacklogDB.failure_count),
            desc(FailedTestBacklogDB.last_failed_at)
        ).offset(offset).limit(limit).all()

    def get_backlog_count(
        self,
        status: Optional[str] = None,
        service: Optional[str] = None
    ) -> int:
        """Zaehlt Backlog-Eintraege."""
        query = self.db.query(func.count(FailedTestBacklogDB.id))
        if status:
            query = query.filter(FailedTestBacklogDB.status == status)
        if service:
            query = query.filter(FailedTestBacklogDB.service == service)
        return query.scalar() or 0

    def get_backlog_item(self, backlog_id: int) -> Optional[FailedTestBacklogDB]:
        """Holt einen einzelnen Backlog-Eintrag."""
        return self.db.query(FailedTestBacklogDB).filter(FailedTestBacklogDB.id == backlog_id).first()

    def update_backlog_status(
        self,
        backlog_id: int,
        status: str,
        notes: Optional[str] = None,
        assigned_to: Optional[str] = None
    ) -> Optional[FailedTestBacklogDB]:
        """Aktualisiert den Status eines Backlog-Eintrags."""
        backlog = self.get_backlog_item(backlog_id)
        if backlog:
            backlog.status = status
            if notes:
                backlog.notes = notes
            if assigned_to:
                backlog.assigned_to = assigned_to
            self.db.commit()
            self.db.refresh(backlog)
        return backlog

    def update_backlog_priority(self, backlog_id: int, priority: str) -> Optional[FailedTestBacklogDB]:
        """Aktualisiert die Prioritaet eines Backlog-Eintrags."""
        backlog = self.get_backlog_item(backlog_id)
        if backlog:
            backlog.priority = priority
            self.db.commit()
            self.db.refresh(backlog)
        return backlog

    # ========================================
    # Fix History
    # ========================================

    def add_fix_attempt(
        self,
        backlog_id: int,
        fix_type: str,
        fix_description: str,
        commit_hash: Optional[str] = None,
        success: bool = False
    ) -> TestFixHistoryDB:
        """Fuegt einen Fix-Versuch zur Historie hinzu."""
        fix = TestFixHistoryDB(
            backlog_id=backlog_id,
            fix_type=fix_type,
            fix_description=fix_description,
            commit_hash=commit_hash,
            success=success
        )
        self.db.add(fix)

        # Bei Erfolg: Backlog-Status aktualisieren
        if success:
            backlog = self.get_backlog_item(backlog_id)
            if backlog:
                backlog.status = "fixed"

        self.db.commit()
        self.db.refresh(fix)
        return fix

    def get_fix_history(self, backlog_id: int) -> List[TestFixHistoryDB]:
        """Holt die Fix-Historie fuer einen Backlog-Eintrag."""
        return self.db.query(TestFixHistoryDB).filter(
            TestFixHistoryDB.backlog_id == backlog_id
        ).order_by(desc(TestFixHistoryDB.created_at)).all()

    # ========================================
    # Service Statistics
    # ========================================

    def _update_service_stats(self, run: TestRunDB):
        """Aktualisiert die Service-Statistiken nach einem Run."""
        stats = self.db.query(TestServiceStatsDB).filter(
            TestServiceStatsDB.service == run.service
        ).first()

        if not stats:
            stats = TestServiceStatsDB(service=run.service)
            self.db.add(stats)

        stats.total_tests = run.total_tests
        stats.passed_tests = run.passed_tests
        stats.failed_tests = run.failed_tests
        stats.skipped_tests = run.skipped_tests
        stats.pass_rate = (run.passed_tests / run.total_tests * 100) if run.total_tests > 0 else 0.0
        stats.last_run_id = run.run_id
        stats.last_run_at = run.completed_at or datetime.utcnow()
        stats.last_status = run.status

        self.db.commit()

    def get_service_stats(self, service: str) -> Optional[TestServiceStatsDB]:
        """Holt Statistiken fuer einen Service."""
        return self.db.query(TestServiceStatsDB).filter(
            TestServiceStatsDB.service == service
        ).first()

    def get_all_service_stats(self) -> List[TestServiceStatsDB]:
        """Holt Statistiken fuer alle Services."""
        return self.db.query(TestServiceStatsDB).all()

    # ========================================
    # History & Trends
    # ========================================

    def get_run_history(
        self,
        service: Optional[str] = None,
        days: int = 30,
        limit: int = 100
    ) -> List[Dict[str, Any]]:
        """
        Holt die Run-Historie fuer Trend-Analysen.
        Gruppiert nach Tag.
        """
        from datetime import timedelta
        cutoff = datetime.utcnow() - timedelta(days=days)

        query = self.db.query(
            func.date(TestRunDB.started_at).label('date'),
            TestRunDB.service,
            func.count(TestRunDB.id).label('runs'),
            func.sum(TestRunDB.total_tests).label('total_tests'),
            func.sum(TestRunDB.passed_tests).label('passed'),
            func.sum(TestRunDB.failed_tests).label('failed')
        ).filter(TestRunDB.started_at >= cutoff)

        if service:
            query = query.filter(TestRunDB.service == service)

        results = query.group_by(
            func.date(TestRunDB.started_at),
            TestRunDB.service
        ).order_by(desc(func.date(TestRunDB.started_at))).limit(limit).all()

        return [
            {
                "date": str(r.date),
                "service": r.service,
                "runs": r.runs,
                "total_tests": r.total_tests or 0,
                "passed": r.passed or 0,
                "failed": r.failed or 0,
                "pass_rate": round((r.passed / r.total_tests * 100) if r.total_tests else 0, 1)
            }
            for r in results
        ]

    def get_summary_stats(self) -> Dict[str, Any]:
        """Holt aggregierte Statistiken ueber alle Services."""
        stats = self.db.query(
            func.sum(TestServiceStatsDB.total_tests).label('total_tests'),
            func.sum(TestServiceStatsDB.passed_tests).label('passed'),
            func.sum(TestServiceStatsDB.failed_tests).label('failed'),
            func.sum(TestServiceStatsDB.skipped_tests).label('skipped'),
            func.count(TestServiceStatsDB.id).label('services_count')
        ).first()

        total = stats.total_tests or 0
        passed = stats.passed or 0

        return {
            "total_tests": total,
            "total_passed": passed,
            "total_failed": stats.failed or 0,
            "total_skipped": stats.skipped or 0,
            "services_count": stats.services_count or 0,
            "overall_pass_rate": round((passed / total * 100) if total > 0 else 0, 1)
        }

    # ========================================
    # Migration Helper
    # ========================================

    def migrate_from_json(self, persisted_results: Dict[str, Dict]) -> int:
        """
        Migriert bestehende JSON-Daten in die Datenbank.
        Wird einmalig beim Upgrade ausgefuehrt.
        """
        count = 0
        for service, data in persisted_results.items():
            # Service-Stats aktualisieren
            stats = self.db.query(TestServiceStatsDB).filter(
                TestServiceStatsDB.service == service
            ).first()

            if not stats:
                stats = TestServiceStatsDB(service=service)
                self.db.add(stats)

            stats.total_tests = data.get("total", 0)
            stats.passed_tests = data.get("passed", 0)
            stats.failed_tests = data.get("failed", 0)
            stats.pass_rate = (stats.passed_tests / stats.total_tests * 100) if stats.total_tests > 0 else 0.0

            last_run = data.get("last_run")
            if last_run:
                try:
                    stats.last_run_at = datetime.fromisoformat(last_run)
                except:
                    stats.last_run_at = datetime.utcnow()

            stats.last_status = data.get("status", "unknown")

            # Fehlgeschlagene Tests ins Backlog
            for failed in data.get("failed_test_ids", []):
                if isinstance(failed, dict):
                    test_name = failed.get("id") or failed.get("name", "unknown")
                    existing = self.db.query(FailedTestBacklogDB).filter(
                        FailedTestBacklogDB.test_name == test_name,
                        FailedTestBacklogDB.service == service
                    ).first()

                    if not existing:
                        backlog = FailedTestBacklogDB(
                            test_name=test_name,
                            test_file=failed.get("file_path"),
                            service=service,
                            error_message=failed.get("error_message"),
                            error_type=failed.get("error_type"),
                            first_failed_at=stats.last_run_at or datetime.utcnow(),
                            last_failed_at=stats.last_run_at or datetime.utcnow(),
                            failure_count=1,
                            status="open",
                            priority=self._calculate_priority(failed.get("error_type")),
                            fix_suggestion=failed.get("suggestion")
                        )
                        self.db.add(backlog)

            count += 1

        self.db.commit()
        return count
