"""
scan_recent.py — Pull & scan repos with commits in the last N days.
Appends a structured run entry to scanned.md in the project root after every run.
"""
import os
import re
import sys
import logging
from pathlib import Path
from datetime import datetime
from typing import Dict, List, Optional

try:
    from config_manager import ConfigManager
    from bitbucket_client import BitbucketClient
    from git_manager import GitManager
    from sonarqube_scanner import SonarQubeScanner
    from report_generator import ReportGenerator
    from scan_report_email import maybe_send_scan_summary_email
except ImportError:
    from .config_manager import ConfigManager
    from .bitbucket_client import BitbucketClient
    from .git_manager import GitManager
    from .sonarqube_scanner import SonarQubeScanner
    from .report_generator import ReportGenerator
    from .scan_report_email import maybe_send_scan_summary_email

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
    handlers=[logging.StreamHandler(sys.stdout)],
)
logger = logging.getLogger(__name__)


def _get_additional_repositories(config: ConfigManager) -> List[Dict]:
    """Get additional repositories from config (same logic as main.py)."""
    additional_repos_config = config.get("scanning.additional_repositories", [])
    if not additional_repos_config:
        return []

    repos = []
    for repo_config in additional_repos_config:
        if isinstance(repo_config, str):
            if repo_config.startswith("git@bitbucket.org:"):
                path = repo_config.replace("git@bitbucket.org:", "").replace(".git", "")
                parts = path.split("/")
                if len(parts) == 2:
                    workspace, repo_name = parts
                    repos.append({
                        "name": repo_name,
                        "slug": repo_name,
                        "workspace": {"slug": workspace},
                        "links": {"clone": [{"name": "ssh", "href": repo_config}]},
                    })
            elif repo_config.startswith("https://github.com/"):
                path = repo_config.replace("https://github.com/", "").replace(".git", "")
                parts = path.split("/")
                if len(parts) == 2:
                    org, repo_name = parts
                    repos.append({
                        "name": repo_name,
                        "slug": repo_name,
                        "workspace": {"slug": org},
                        "links": {
                            "clone": [
                                {"name": "ssh", "href": repo_config},
                                {"name": "https", "href": repo_config},
                            ]
                        },
                    })
            elif "/" in repo_config:
                parts = repo_config.split("/")
                if len(parts) == 2:
                    workspace, repo_name = parts
                    ssh_url = f"git@bitbucket.org:{workspace}/{repo_name}.git"
                    repos.append({
                        "name": repo_name,
                        "slug": repo_name,
                        "workspace": {"slug": workspace},
                        "links": {"clone": [{"name": "ssh", "href": ssh_url}]},
                    })
        elif isinstance(repo_config, dict):
            repo_name = repo_config.get("name") or repo_config.get("slug")
            workspace = repo_config.get("workspace")
            ssh_url = repo_config.get("ssh_url") or repo_config.get("clone_url")
            if repo_name and ssh_url:
                if isinstance(workspace, str):
                    workspace = {"slug": workspace}
                elif not workspace and "git@bitbucket.org:" in ssh_url:
                    path = ssh_url.replace("git@bitbucket.org:", "").replace(".git", "")
                    ws_name = path.split("/")[0] if "/" in path else None
                    workspace = {"slug": ws_name} if ws_name else {}
                repos.append({
                    "name": repo_name,
                    "slug": repo_name,
                    "workspace": workspace or {},
                    "links": {"clone": [{"name": "ssh", "href": ssh_url}]},
                })
    return repos


def _merge_scan_recent_always_include(
    config: ConfigManager,
    all_repos: List[Dict],
    to_process: List[Dict],
) -> None:
    """Ensure listed slugs are pulled/scanned even if commit-window logic skipped them (must appear in all_repos)."""
    raw = config.get("scanning.scan_recent_always_include") or []
    if isinstance(raw, str):
        raw = [s.strip() for s in raw.split(",") if s.strip()]
    if not raw:
        return
    seen = {
        (r.get("name") or r.get("slug"))
        for r in to_process
        if r.get("name") or r.get("slug")
    }
    for slug in raw:
        if not slug or slug in seen:
            continue
        for repo_info in all_repos:
            rslug = repo_info.get("name") or repo_info.get("slug")
            if rslug == slug:
                to_process.append(repo_info)
                seen.add(slug)
                logger.info(f"Including repo (scan_recent_always_include): {slug}")
                break
        else:
            logger.warning(
                "scan_recent_always_include: slug %r not in repo list — add scanning.additional_repositories entry",
                slug,
            )


def _format_scan_summary_section(
    days: int,
    to_process: List[Dict],
    pull_results: List[tuple],
    scan_results: List[tuple],
    run_ts: Optional[str] = None,
) -> str:
    """Markdown block matching scanned.md run sections (also used for email body)."""
    if run_ts is None:
        run_ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")

    pull_ok = sum(1 for _, ok, _ in pull_results if ok)
    scan_ok = sum(1 for _, ok, _, _r in scan_results if ok)

    pull_rows = ""
    for repo_slug, ok, msg in pull_results:
        status = "✅ OK" if ok else "❌ FAIL"
        notes = "" if ok else msg
        pull_rows += f"| {repo_slug} | {status} | {notes} |\n"

    scan_rows = ""
    for repo_slug, ok, msg, report_path in scan_results:
        status = "✅ OK" if ok else "❌ FAIL"
        notes = "" if ok else msg
        report_cell = Path(report_path).name if report_path else ""
        scan_rows += f"| {repo_slug} | {status} | {report_cell} | {notes} |\n"

    return f"""## Run — {run_ts}

**Repos with commits in last {days} days:** {len(to_process)}

### Git Pulls

| Repository | Status | Notes |
|---|---|---|
{pull_rows}
**Pulled: {pull_ok} / {len(pull_results)}**

### SonarQube Scans

| Repository | Status | Report | Notes |
|---|---|---|---|
{scan_rows}
**Scanned: {scan_ok} / {len(scan_results)}**

---

"""


def _append_to_scanned_md(
    project_root: Path,
    days: int,
    to_process: List[Dict],
    pull_results: List[tuple],
    scan_results: List[tuple],
) -> str:
    """Append a new run section to scanned.md; return the section text for email."""
    scanned_md = project_root / "scanned.md"
    run_ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    section = _format_scan_summary_section(days, to_process, pull_results, scan_results, run_ts)

    first_run = not scanned_md.exists()
    with open(scanned_md, "a", encoding="utf-8") as f:
        if first_run:
            f.write("# Scan History\n\n")
        f.write(section)

    logger.info(f"Appended run entry to {scanned_md}")
    return section


def _send_summary_email(section: str, log_file: Path) -> None:
    """Notify recipients when SMTP_REPORT_* is configured."""
    body = f"Sonar scan_recent summary\n\n{section}\nFull log: {log_file}\n"
    ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    maybe_send_scan_summary_email(f"[Sonar] scan_recent finished {ts}", body)


def run_scan_recent() -> None:
    """Pull and scan repos with commits in last N days; append results to scanned.md."""
    project_root = Path(__file__).resolve().parent.parent
    if str(project_root) not in sys.path:
        sys.path.insert(0, str(project_root))

    os.chdir(project_root)

    # Log file for this run
    log_dir = project_root / "logs"
    log_dir.mkdir(parents=True, exist_ok=True)
    log_ts = datetime.now().strftime("%Y%m%d_%H%M%S")
    log_file = log_dir / f"scan_recent_{log_ts}.log"
    file_handler = logging.FileHandler(log_file, encoding="utf-8")
    file_handler.setFormatter(logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s"))
    logging.getLogger().addHandler(file_handler)

    logger.info("Starting scan_recent (repos with commits in last N days)")

    try:
        config = ConfigManager()
    except Exception as e:
        logger.error(f"Failed to load config: {e}")
        sys.exit(1)

    # Validate required config
    if not config.get("bitbucket.workspace"):
        logger.error("Bitbucket workspace not configured")
        sys.exit(1)
    has_api_token = bool(config.get("bitbucket.api_token"))
    has_app_password = bool(config.get("bitbucket.app_password"))
    if not config.get("bitbucket.username"):
        logger.error("Bitbucket username/email not configured")
        sys.exit(1)
    if not has_api_token and not has_app_password:
        logger.error("Bitbucket authentication not configured (need API token or app password)")
        sys.exit(1)
    if not config.get("sonarqube.token"):
        logger.error("SonarQube token not configured")
        sys.exit(1)

    import json
    history_file = Path(config.get("scanning.history_file") or "scan_history.json")
    scan_history = {}
    if history_file.exists():
        try:
            scan_history = json.loads(history_file.read_text(encoding="utf-8"))
        except Exception:
            pass

    days = config.get("scanning.weekly_scan_days") or 7
    main_workspace = config.get("bitbucket.workspace")
    repos_dir = Path(config.get("scanning.repos_dir"))

    # Init clients
    bitbucket = BitbucketClient(
        workspace=main_workspace,
        username=config.get("bitbucket.username"),
        app_password=config.get("bitbucket.app_password") if not has_api_token else None,
        api_token=config.get("bitbucket.api_token"),
    )
    git_manager = GitManager(repos_dir=config.get("scanning.repos_dir"))
    sonar_scanner = SonarQubeScanner(
        sonar_url=config.get("sonarqube.url"),
        sonar_token=config.get("sonarqube.token"),
        scanner_path=config.get("scanning.sonar_scanner_path"),
    )
    report_generator = ReportGenerator(reports_dir=config.get("scanning.reports_dir"))

    # Get all repos
    repos = bitbucket.get_all_repositories() or []
    additional = _get_additional_repositories(config)
    if additional:
        logger.info(f"Found {len(additional)} additional repositories from config")
        repos.extend(additional)

    if not repos:
        logger.warning("No repositories found")
        section = _append_to_scanned_md(project_root, days, [], [], [])
        _send_summary_email(section, log_file)
        return

    # Filter: repos with commits in last N days
    to_process = []
    for repo_info in repos:
        repo_slug = repo_info.get("name") or repo_info.get("slug")
        if not repo_slug:
            continue
        ws_slug = (repo_info.get("workspace") or {}).get("slug")
        if ws_slug and ws_slug != main_workspace:
            # Additional repo from different workspace: include without commit check
            to_process.append(repo_info)
            logger.info(f"Including additional repo (no commit check): {repo_slug}")
        elif bitbucket.has_commits_in_last_n_days(repo_slug, days=days):
            to_process.append(repo_info)
            logger.info(f"Including repo (commits in last {days} days): {repo_slug}")
    _merge_scan_recent_always_include(config, repos, to_process)
    logger.info(f"Repos to pull and scan: {len(to_process)}")

    # Phase 1: Pull
    pull_results = []
    for repo_info in to_process:
        repo_slug = repo_info.get("name") or repo_info.get("slug")
        clone_url = bitbucket.get_clone_url(repo_info, protocol="ssh")
        if not clone_url:
            logger.warning(f"No SSH clone URL for {repo_slug}, skipping")
            pull_results.append((repo_slug, False, "No SSH clone URL"))
            continue
        try:
            result = git_manager.clone_or_update(repo_slug=repo_slug, clone_url=clone_url)
            ok = result and (isinstance(result, tuple) and result[0])
            pull_results.append((repo_slug, ok, "OK" if ok else "Pull failed"))
            if ok:
                logger.info(f"Pull OK: {repo_slug}")
            else:
                logger.warning(f"Pull failed: {repo_slug}")
        except Exception as e:
            logger.exception(f"Pull error for {repo_slug}: {e}")
            pull_results.append((repo_slug, False, str(e)))

    # Phase 2: Scan
    scan_results = []
    for repo_info in to_process:
        repo_slug = repo_info.get("name") or repo_info.get("slug")
        repo_path = repos_dir / repo_slug
        if not repo_path.exists() or not (repo_path / ".git").exists():
            scan_results.append((repo_slug, False, "Repo missing or not a git repo", None))
            continue

        sonar_props_path = git_manager.get_sonar_properties_path(repo_path)
        if not sonar_props_path:
            workspace = repo_info.get("workspace", {}).get("slug") or main_workspace
            project_key = f"{workspace}_{repo_slug}".lower().replace("-", "_").replace(" ", "_")
            project_key = re.sub(r"[^a-z0-9_]", "_", project_key)
            project_name = repo_info.get("name") or repo_slug
            try:
                sonar_props_path = sonar_scanner.generate_sonar_properties(
                    repo_path=repo_path,
                    project_key=project_key,
                    project_name=project_name,
                )
            except Exception as e:
                logger.error(f"Failed to generate sonar-project.properties for {repo_slug}: {e}")
                scan_results.append((repo_slug, False, str(e), None))
                continue

        project_key = sonar_scanner.get_project_key(sonar_props_path)
        if not project_key:
            scan_results.append((repo_slug, False, "No project key in sonar-project.properties", None))
            continue

        if not sonar_scanner.check_project_exists(project_key):
            project_name = repo_info.get("name") or repo_slug
            description = repo_info.get("description", "") or ""
            sonar_scanner.create_project(
                project_key=project_key,
                project_name=project_name,
                description=description,
            )

        try:
            result = sonar_scanner.run_scan(repo_path, sonar_props_path)
            success = result.get("success", False)
            err = result.get("error", "") if not success else ""

            # Fetch metrics and generate PDF report
            sonar_results = None
            if success:
                import time
                time.sleep(5)  # let SonarQube process the task
                sonar_results = sonar_scanner.get_scan_results(project_key)

            report_path = None
            try:
                report_path = report_generator.generate_report(
                    repo_name=repo_slug,
                    scan_results=result,
                    sonar_results=sonar_results,
                    scan_output=result.get("output"),
                )
                logger.info(f"Report generated: {report_path}")
            except Exception as e:
                logger.error(f"Failed to generate report for {repo_slug}: {e}")

            # Update scan history
            scan_history[repo_slug] = {
                "last_scan": datetime.now().isoformat(),
                "success": success,
                "report_path": str(report_path) if report_path else None,
            }
            try:
                history_file.write_text(json.dumps(scan_history, indent=2), encoding="utf-8")
            except Exception as e:
                logger.warning(f"Failed to save scan history: {e}")

            scan_results.append((repo_slug, success, err or ("OK" if success else "Scan failed"), str(report_path) if report_path else None))
            if success:
                logger.info(f"Scan OK: {repo_slug}")
            else:
                logger.warning(f"Scan failed: {repo_slug} - {err}")
        except Exception as e:
            logger.exception(f"Scan error for {repo_slug}: {e}")
            scan_results.append((repo_slug, False, str(e), None))

    section = _append_to_scanned_md(project_root, days, to_process, pull_results, scan_results)
    _send_summary_email(section, log_file)
    logger.info(f"scan_recent finished. Log: {log_file}")


if __name__ == "__main__":
    try:
        run_scan_recent()
    except KeyboardInterrupt:
        logger.info("Interrupted by user")
        sys.exit(1)
    except Exception as e:
        logger.exception(f"Fatal error: {e}")
        sys.exit(1)
