"""Pipeline orchestrator — the full `antiqa run JIRA-123` execution engine.

Two execution paths:
  Path A  (test cases attached):  download → parse → AI interpret → execute → report
  Path B  (no test cases):        AI analyse ticket → select flows → execute → report
"""

from __future__ import annotations

import logging
from pathlib import Path
from typing import Any, Callable

from antiqa.agents.failure_analyzer import BugReport, FailureAnalyzerAgent
from antiqa.agents.test_case_interpreter import TestCaseInterpreterAgent
from antiqa.agents.ticket_analyzer import TestPlan, TicketAnalyzerAgent
from antiqa.browser.manager import BrowserManager
from antiqa.config.schema import AntiQAConfig
from antiqa.crawler.graph import NavigationGraph
from antiqa.flows.parser import FlowDefinition, FlowParser
from antiqa.flows.runner import FlowResult, FlowRunner
from antiqa.jira.agent import JiraAgent
from antiqa.jira.attachments import AttachmentDownloader
from antiqa.jira.bug_reporter import BugReporter
from antiqa.jira.test_case_parser import TestCaseFileParser

from .result import BugFiled, ExecutionPath, PipelineResult

log = logging.getLogger("antiqa.pipeline")

ProgressCallback = Callable[[str, str], None]  # (phase, detail)


class Pipeline:
    """Orchestrate the full test-from-Jira pipeline."""

    def __init__(
        self,
        config: AntiQAConfig,
        jira_agent: JiraAgent,
        headed: bool | None = None,
        record_video: bool = False,
        on_progress: ProgressCallback | None = None,
    ) -> None:
        self.config = config
        self.jira = jira_agent
        self.record_video = record_video
        self._progress = on_progress or (lambda *_: None)

        if headed is not None:
            self.config.browser.headless = not headed

        # Try to load the navigation graph if it exists
        self.graph: NavigationGraph | None = None
        graph_path = Path("logs/navigation_graph.json")
        if graph_path.is_file():
            try:
                self.graph = NavigationGraph.load(graph_path)
            except Exception:
                pass

    def run(self, ticket_key: str) -> PipelineResult:
        """Execute the full pipeline for a Jira ticket."""
        result = PipelineResult(ticket_key=ticket_key, execution_path=ExecutionPath.TEST_CASES)

        try:
            # 1. Fetch ticket
            self._progress("fetch", f"Fetching {ticket_key} from Jira...")
            issue = self.jira.get_issue(ticket_key)
            if "error" in issue:
                result.error = f"Failed to fetch ticket: {issue['error']}"
                return result

            # 2. Resolve test flows (test cases take priority over description)
            self._progress("attachments", "Checking for test case attachments...")
            flows = self._resolve_flows(ticket_key, issue, result)

            if not flows:
                result.error = "No test flows could be determined for this ticket."
                return result

            # 3. Execute flows in the browser
            self._progress("browser", "Launching browser...")
            flow_results = self._execute_flows(flows)
            result.flow_results = flow_results

            # 4. Handle test step failures
            failed = [fr for fr in flow_results if not fr.passed]
            if failed:
                self._progress("analysis", f"Analysing {len(failed)} failure(s)...")
                self._handle_failures(ticket_key, failed, result)
            else:
                self._progress("report", "All tests passed — posting summary...")
                self._post_success(ticket_key, result)

            # 5. Handle health issues (even if steps passed)
            flows_with_health = [fr for fr in flow_results if fr.critical_health_issues]
            if flows_with_health:
                self._progress("health", f"Filing bugs for {sum(len(fr.critical_health_issues) for fr in flows_with_health)} runtime health issue(s)...")
                self._handle_health_issues(ticket_key, flows_with_health, result)

            # 6. Clean up screenshots after bugs are filed
            self._cleanup_screenshots()

        except Exception as exc:
            log.exception("Pipeline error for %s", ticket_key)
            result.error = str(exc)

        return result

    def dry_run(self, ticket_key: str) -> dict[str, Any]:
        """Show the test plan without executing (no browser, no Jira updates)."""
        self._progress("fetch", f"Fetching {ticket_key} from Jira...")
        issue = self.jira.get_issue(ticket_key)
        if "error" in issue:
            return {"error": issue["error"]}

        plan: dict[str, Any] = {
            "ticket": ticket_key,
            "summary": issue.get("summary", ""),
        }

        downloader = AttachmentDownloader(self.jira)
        tc_files = downloader.get_test_case_attachments(ticket_key)

        if tc_files:
            plan["path"] = "test_cases"
            plan["attachments"] = [a["filename"] for a in tc_files]

            files = downloader.download_all(ticket_key)
            parser = TestCaseFileParser()
            cases = parser.parse_many(files)
            plan["test_cases"] = [
                {"id": tc.id, "title": tc.title, "steps": len(tc.steps)}
                for tc in cases
            ]

            interpreter = TestCaseInterpreterAgent(self.config, graph=self.graph)
            flows = interpreter.interpret_many(cases)
            plan["flows"] = [
                {"name": f.name, "steps": len(f.steps), "description": f.description}
                for f in flows
            ]
        else:
            plan["path"] = "ticket_analysis"
            analyzer = TicketAnalyzerAgent(self.config)
            test_plan = analyzer.analyze(
                issue.get("summary", ""),
                issue.get("description", ""),
            )
            plan["test_plan"] = {
                "flows": test_plan.flows,
                "reasoning": test_plan.reasoning,
                "risk_areas": test_plan.risk_areas,
            }

        return plan

    # ── Path resolution ────────────────────────────────

    def _resolve_flows(
        self, ticket_key: str, issue: dict, result: PipelineResult
    ) -> list[FlowDefinition]:
        """Determine which flows to run.

        Priority order:
          1. Attached test case files (Excel/CSV) — most reliable, written by QA
          2. Ticket description analysis — fallback when no test cases exist
        """
        downloader = AttachmentDownloader(self.jira)
        tc_files = downloader.get_test_case_attachments(ticket_key)

        if tc_files:
            self._progress("attachments", f"Found {len(tc_files)} test case file(s) — using attached test cases")
            flows = self._path_a(ticket_key, downloader, result)
            if flows:
                return flows
            log.warning("Test case files found but produced no flows, trying description fallback")

        self._progress("analyze", "No test case files attached — falling back to ticket description analysis")
        return self._path_b(issue, result)

    def _path_a(
        self, ticket_key: str, downloader: AttachmentDownloader, result: PipelineResult
    ) -> list[FlowDefinition]:
        """Path A: test case files attached."""
        result.execution_path = ExecutionPath.TEST_CASES
        self._progress("download", "Downloading test case files...")

        files = downloader.download_all(ticket_key)
        if not files:
            return []

        self._progress("parse", f"Parsing {len(files)} test case file(s)...")
        parser = TestCaseFileParser()
        cases = parser.parse_many(files)

        if not cases:
            return []

        self._progress("interpret", f"AI interpreting {len(cases)} test case(s)...")
        interpreter = TestCaseInterpreterAgent(self.config, graph=self.graph)
        flows = interpreter.interpret_many(cases)

        log.info("Path A: %d test case(s) → %d flow(s)", len(cases), len(flows))
        return flows

    def _path_b(
        self, issue: dict, result: PipelineResult
    ) -> list[FlowDefinition]:
        """Path B (fallback): no test cases — AI selects predefined flows from description."""
        result.execution_path = ExecutionPath.TICKET_ANALYSIS
        self._progress("analyze", "AI analysing ticket description to select test flows...")

        analyzer = TicketAnalyzerAgent(self.config)
        plan = analyzer.analyze(
            issue.get("summary", ""),
            issue.get("description", ""),
        )

        if not plan.flows:
            return []

        self._progress("load", f"Loading {len(plan.flows)} flow(s): {', '.join(plan.flows)}")
        flow_parser = FlowParser(self.config.flows_dir)
        flows: list[FlowDefinition] = []
        for name in plan.flows:
            try:
                flows.append(flow_parser.resolve_with_includes(name))
            except FileNotFoundError:
                log.warning("Flow '%s' selected by AI but not found on disk", name)

        log.info("Path B: AI selected %d flow(s)", len(flows))
        return flows

    # ── Execution ──────────────────────────────────────

    def _execute_flows(self, flows: list[FlowDefinition]) -> list[FlowResult]:
        """Launch browser and execute all flows sequentially."""
        results: list[FlowResult] = []
        mgr = BrowserManager(self.config.browser, logs_dir=self.config.logs_dir)
        page = mgr.launch(record_video=self.record_video)

        try:
            runner = FlowRunner(
                page, config=self.config,
                screenshots_dir=f"{self.config.logs_dir}/screenshots",
            )
            for i, flow in enumerate(flows, 1):
                self._progress("execute", f"Running flow {i}/{len(flows)}: {flow.name}")
                fr = runner.run(flow, stop_on_failure=True)
                results.append(fr)
                log.info("Flow %s: %s", flow.name, "PASS" if fr.passed else "FAIL")
        finally:
            mgr.close()

        return results

    # ── Failure handling ───────────────────────────────

    def _handle_failures(
        self, ticket_key: str, failed: list[FlowResult], result: PipelineResult
    ) -> None:
        """Analyse failures, generate bug reports, file Jira sub-tasks."""
        analyzer = FailureAnalyzerAgent(self.config)
        reporter = BugReporter(self.jira)

        for fr in failed:
            reports = analyzer.analyze_flow_result(fr)
            result.bug_reports.extend(reports)

            for i, report in enumerate(reports):
                failed_step = fr.failed_steps[i] if i < len(fr.failed_steps) else None
                screenshot_path = None
                if failed_step and failed_step.action_result.screenshot_path:
                    sp = Path(failed_step.action_result.screenshot_path)
                    if sp.exists():
                        screenshot_path = sp

                self._progress("bug", f"Filing bug: {report.summary[:60]}...")
                bug_result = reporter.report_failure(
                    parent_key=ticket_key,
                    test_case_title=f"{fr.flow_name}",
                    failure_summary=report.summary,
                    failed_step=report.steps_to_reproduce[0] if report.steps_to_reproduce else "Unknown step",
                    expected=report.expected_result,
                    actual=report.actual_result,
                    screenshot_path=screenshot_path,
                    priority=analyzer.severity_to_priority(report.severity),
                )

                if "key" in bug_result:
                    result.bugs_filed.append(BugFiled(
                        jira_key=bug_result["key"],
                        jira_url=bug_result.get("url", ""),
                        summary=report.summary,
                        severity=report.severity,
                        flow_name=fr.flow_name,
                        step_number=failed_step.step_number if failed_step else 0,
                    ))

    def _handle_health_issues(
        self, ticket_key: str, flows_with_health: list[FlowResult], result: PipelineResult
    ) -> None:
        """File Jira bugs for runtime health issues detected during testing."""
        reporter = BugReporter(self.jira)

        for fr in flows_with_health:
            bug_results = reporter.report_health_issues(
                parent_key=ticket_key,
                flow_name=fr.flow_name,
                health_issues=fr.health_issues,
            )

            for br in bug_results:
                if "key" in br:
                    result.bugs_filed.append(BugFiled(
                        jira_key=br["key"],
                        jira_url=br.get("url", ""),
                        summary=f"Health issues in {fr.flow_name}",
                        severity="health",
                        flow_name=fr.flow_name,
                        step_number=0,
                    ))

    def _cleanup_screenshots(self) -> None:
        """Remove all screenshots from the project after bugs have been filed."""
        from antiqa.utils.cleanup import remove_screenshots

        removed = remove_screenshots(f"{self.config.logs_dir}/screenshots")
        if removed:
            self._progress("cleanup", f"Removed {removed} screenshot(s) from project")

    def _post_success(self, ticket_key: str, result: PipelineResult) -> None:
        """Post a success comment on the Jira ticket."""
        s = result.summary()
        health_note = ""
        if s["health_issues"] > 0:
            health_note = (
                f"\n\n⚠️ *Runtime health issues detected:* "
                f"{s['health_critical']} critical, {s['health_warnings']} warnings\n"
                f"_Separate bug tickets will be filed for critical issues._"
            )

        comment = (
            f"*AntiQA — All Tests Passed* ✅\n\n"
            f"Flows: {s['flows_passed']}/{s['flows_total']} passed\n"
            f"Steps: {s['steps_passed']}/{s['steps_total']} passed\n"
            f"Duration: {s['duration_ms']:.0f}ms"
            f"{health_note}\n\n"
            f"_Execution path: {s['path']}_"
        )
        self.jira.add_comment(ticket_key, comment)
